├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── pagedown-bootstrap-rails.rb └── pagedown_bootstrap │ ├── rails.rb │ └── rails │ ├── engine.rb │ └── version.rb ├── pagedown-bootstrap-rails.gemspec └── vendor └── assets ├── javascripts ├── markdown.converter.js ├── markdown.editor.js.erb ├── markdown.extra.js ├── markdown.sanitizer.js ├── pagedown_bootstrap.js └── pagedown_init.js.coffee.erb └── stylesheets └── pagedown_bootstrap.scss /.gitignore: -------------------------------------------------------------------------------- 1 | .pkg/ 2 | *.gem 3 | Gemfile.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Hugh Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PageDown Bootstrap Rails 2 | 3 | A Ruby gem version of [https://github.com/tchapi/pagedown-bootstrap](https://github.com/tchapi/pagedown-bootstrap) for the Rails asset pipeline. 4 | 5 | ## Installation 6 | 7 | Add to your `Gemfile`: 8 | 9 | ``` ruby 10 | gem 'pagedown-bootstrap-rails' 11 | ``` 12 | 13 | You’ll also need Bootstrap 3 (Sass version) and Font Awesome. 14 | 15 | ## Usage 16 | 17 | Require the CSS with Sprockets: 18 | 19 | ``` css 20 | /*= require pagedown_bootstrap */ 21 | ``` 22 | 23 | Or with an SCSS import: 24 | 25 | ``` scss 26 | @import "pagedown_bootstrap"; 27 | ``` 28 | 29 | Sprockets require the JS too: 30 | 31 | ``` javascript 32 | //= require pagedown_bootstrap 33 | ``` 34 | 35 | Or individually as you please: 36 | 37 | ``` javascript 38 | //= require markdown.converter 39 | //= require markdown.editor 40 | //= require markdown.sanitizer 41 | //= require markdown.extra 42 | ``` 43 | 44 | You will need to initialize PageDown in your form, so PageDown Bootstrap Rails comes with `pagedown_init` for you to optionally include: 45 | 46 | ``` coffee 47 | $ -> 48 | $('textarea.wmd-input').each (i, input) -> 49 | attr = $(input).attr('id').split('wmd-input')[1] 50 | converter = new Markdown.Converter() 51 | Markdown.Extra.init(converter) 52 | help = 53 | handler: () -> 54 | window.open('http://daringfireball.net/projects/markdown/syntax') 55 | return false 56 | title: "<%= I18n.t('components.markdown_editor.help', default: 'Markdown Editing Help') %>" 57 | editor = new Markdown.Editor(converter, attr, help) 58 | editor.run() 59 | ``` 60 | 61 | Just require it after `pagedown_bootstrap`: 62 | 63 | ``` javascript 64 | //= require pagedown_bootstrap 65 | //= require pagedown_init 66 | ``` 67 | 68 | This obviously requires CoffeeScript and jQuery, so if you’re not using these then feel free to write your own initializer. Additionally, if you’re using Turbolinks then I suggest either using [jQuery Turbolinks](https://github.com/kossnocorp/jquery.turbolinks) or writing 69 | your own initializer that does not rely on `jQuery.ready()` like the one above. 70 | 71 | ## SimpleForm 72 | 73 | Here’s a [SimpleForm](https://github.com/plataformatec/simple_form) input that creates the correct HTML for the initializer above. 74 | 75 | ``` ruby 76 | class PagedownInput < SimpleForm::Inputs::TextInput 77 | def input 78 | out = "
#{wmd_input}" 79 | 80 | if input_html_options[:preview] 81 | out << "
" 82 | end 83 | 84 | out.html_safe 85 | end 86 | 87 | private 88 | 89 | def wmd_input 90 | classes = input_html_options[:class] || [] 91 | classes << 'wmd-input form-control' 92 | @builder.text_area( 93 | attribute_name, 94 | input_html_options.merge( 95 | class: classes, id: "wmd-input-#{attribute_name}" 96 | ) 97 | ) 98 | end 99 | end 100 | ``` 101 | 102 | Which you use in your form like so: 103 | 104 | ``` ruby 105 | = f.input :description, as: :pagedown, input_html: { preview: true, rows: 10 } 106 | ``` 107 | 108 | This is how it looks: 109 | 110 | ![Glorious](https://cldup.com/zCzX0kUgrW.png) 111 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /lib/pagedown-bootstrap-rails.rb: -------------------------------------------------------------------------------- 1 | require 'pagedown_bootstrap/rails' 2 | -------------------------------------------------------------------------------- /lib/pagedown_bootstrap/rails.rb: -------------------------------------------------------------------------------- 1 | require 'pagedown_bootstrap/rails/engine' 2 | require 'pagedown_bootstrap/rails/version' 3 | -------------------------------------------------------------------------------- /lib/pagedown_bootstrap/rails/engine.rb: -------------------------------------------------------------------------------- 1 | module PageDownBootstrap 2 | module Rails 3 | class Engine < ::Rails::Engine 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/pagedown_bootstrap/rails/version.rb: -------------------------------------------------------------------------------- 1 | module PageDownBootstrap 2 | module Rails 3 | VERSION = '2.1.4'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /pagedown-bootstrap-rails.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/pagedown_bootstrap/rails/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'pagedown-bootstrap-rails' 5 | s.version = PageDownBootstrap::Rails::VERSION 6 | s.description = 'PageDown Bootstrap for the Rails asset pipeline' 7 | s.summary = 'This gem makes PageDown Bootstrap available in the Rails asset pipeline.' 8 | s.authors = ['Hugh Evans'] 9 | s.email = ['hugh@artpop.com.au'] 10 | s.date = Time.now.strftime('%Y-%m-%d') 11 | s.require_paths = ['lib'] 12 | s.add_dependency('railties', '> 3.1') 13 | s.files = Dir['{lib,vendor}/**/*'] + ['README.md'] 14 | s.homepage = 'http://github.com/hughevans/pagedown-bootstrap-rails' 15 | s.license = 'MIT' 16 | end 17 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/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 (text) { 71 | var args = Array.prototype.slice.call(arguments, 0); 72 | args[0] = original.apply(null, args); 73 | return func.apply(null, args); 74 | }; 75 | }, 76 | set: function (hookname, func) { 77 | if (!this[hookname]) 78 | throw new Error("unknown hook " + hookname); 79 | this[hookname] = func; 80 | }, 81 | addNoop: function (hookname) { 82 | this[hookname] = identity; 83 | }, 84 | addFalse: function (hookname) { 85 | this[hookname] = returnFalse; 86 | } 87 | }; 88 | 89 | Markdown.HookCollection = HookCollection; 90 | 91 | // g_urls and g_titles allow arbitrary user-entered strings as keys. This 92 | // caused an exception (and hence stopped the rendering) when the user entered 93 | // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this 94 | // (since no builtin property starts with "s_"). See 95 | // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug 96 | // (granted, switching from Array() to Object() alone would have left only __proto__ 97 | // to be a problem) 98 | function SaveHash() { } 99 | SaveHash.prototype = { 100 | set: function (key, value) { 101 | this["s_" + key] = value; 102 | }, 103 | get: function (key) { 104 | return this["s_" + key]; 105 | } 106 | }; 107 | 108 | Markdown.Converter = function () { 109 | var pluginHooks = this.hooks = new HookCollection(); 110 | 111 | // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link 112 | pluginHooks.addNoop("plainLinkText"); 113 | 114 | // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked 115 | pluginHooks.addNoop("preConversion"); 116 | 117 | // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have 118 | pluginHooks.addNoop("postNormalization"); 119 | 120 | // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively 121 | // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner 122 | // call will receive outdented text. 123 | pluginHooks.addNoop("preBlockGamut"); 124 | pluginHooks.addNoop("postBlockGamut"); 125 | 126 | // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made 127 | pluginHooks.addNoop("preSpanGamut"); 128 | pluginHooks.addNoop("postSpanGamut"); 129 | 130 | // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml 131 | pluginHooks.addNoop("postConversion"); 132 | 133 | // 134 | // Private state of the converter instance: 135 | // 136 | 137 | // Global hashes, used by various utility routines 138 | var g_urls; 139 | var g_titles; 140 | var g_html_blocks; 141 | 142 | // Used to track when we're inside an ordered or unordered list 143 | // (see _ProcessListItems() for details): 144 | var g_list_level; 145 | 146 | this.makeHtml = function (text) { 147 | 148 | // 149 | // Main function. The order in which other subs are called here is 150 | // essential. Link and image substitutions need to happen before 151 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 152 | // and tags get encoded. 153 | // 154 | 155 | // This will only happen if makeHtml on the same converter instance is called from a plugin hook. 156 | // Don't do that. 157 | if (g_urls) 158 | throw new Error("Recursive call to converter.makeHtml"); 159 | 160 | // Create the private state objects. 161 | g_urls = new SaveHash(); 162 | g_titles = new SaveHash(); 163 | g_html_blocks = []; 164 | g_list_level = 0; 165 | 166 | text = pluginHooks.preConversion(text); 167 | 168 | // attacklab: Replace ~ with ~T 169 | // This lets us use tilde as an escape char to avoid md5 hashes 170 | // The choice of character is arbitray; anything that isn't 171 | // magic in Markdown will work. 172 | text = text.replace(/~/g, "~T"); 173 | 174 | // attacklab: Replace $ with ~D 175 | // RegExp interprets $ as a special character 176 | // when it's in a replacement string 177 | text = text.replace(/\$/g, "~D"); 178 | 179 | // Standardize line endings 180 | text = text.replace(/\r\n/g, "\n"); // DOS to Unix 181 | text = text.replace(/\r/g, "\n"); // Mac to Unix 182 | 183 | // Make sure text begins and ends with a couple of newlines: 184 | text = "\n\n" + text + "\n\n"; 185 | 186 | // Convert all tabs to spaces. 187 | text = _Detab(text); 188 | 189 | // Strip any lines consisting only of spaces and tabs. 190 | // This makes subsequent regexen easier to write, because we can 191 | // match consecutive blank lines with /\n+/ instead of something 192 | // contorted like /[ \t]*\n+/ . 193 | text = text.replace(/^[ \t]+$/mg, ""); 194 | 195 | text = pluginHooks.postNormalization(text); 196 | 197 | // Turn block-level HTML blocks into hash entries 198 | text = _HashHTMLBlocks(text); 199 | 200 | // Strip link definitions, store in hashes. 201 | text = _StripLinkDefinitions(text); 202 | 203 | text = _RunBlockGamut(text); 204 | 205 | text = _UnescapeSpecialChars(text); 206 | 207 | // attacklab: Restore dollar signs 208 | text = text.replace(/~D/g, "$$"); 209 | 210 | // attacklab: Restore tildes 211 | text = text.replace(/~T/g, "~"); 212 | 213 | text = pluginHooks.postConversion(text); 214 | 215 | g_html_blocks = g_titles = g_urls = null; 216 | 217 | return text; 218 | }; 219 | 220 | function _StripLinkDefinitions(text) { 221 | // 222 | // Strips link definitions from text, stores the URLs and titles in 223 | // hash references. 224 | // 225 | 226 | // Link defs are in the form: ^[id]: url "optional title" 227 | 228 | /* 229 | text = text.replace(/ 230 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 231 | [ \t]* 232 | \n? // maybe *one* newline 233 | [ \t]* 234 | ? // url = $2 235 | (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below 236 | [ \t]* 237 | \n? // maybe one newline 238 | [ \t]* 239 | ( // (potential) title = $3 240 | (\n*) // any lines skipped = $4 attacklab: lookbehind removed 241 | [ \t]+ 242 | ["(] 243 | (.+?) // title = $5 244 | [")] 245 | [ \t]* 246 | )? // title is optional 247 | (?:\n+|$) 248 | /gm, function(){...}); 249 | */ 250 | 251 | text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, 252 | function (wholeMatch, m1, m2, m3, m4, m5) { 253 | m1 = m1.toLowerCase(); 254 | g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive 255 | if (m4) { 256 | // Oops, found blank lines, so it's not a title. 257 | // Put back the parenthetical statement we stole. 258 | return m3; 259 | } else if (m5) { 260 | g_titles.set(m1, m5.replace(/"/g, """)); 261 | } 262 | 263 | // Completely remove the definition from the text 264 | return ""; 265 | } 266 | ); 267 | 268 | return text; 269 | } 270 | 271 | function _HashHTMLBlocks(text) { 272 | 273 | // Hashify HTML blocks: 274 | // We only want to do this for block-level HTML tags, such as headers, 275 | // lists, and tables. That's because we still want to wrap

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

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

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

" + _RunSpanGamut(m1) + "

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

    Just type foo `bar` baz at the prompt.

    1017 | // 1018 | // There's no arbitrary limit to the number of backticks you 1019 | // can use as delimters. If you need three consecutive backticks 1020 | // in your code, use four for delimiters, etc. 1021 | // 1022 | // * You can use spaces to get literal backticks at the edges: 1023 | // 1024 | // ... type `` `bar` `` ... 1025 | // 1026 | // Turns to: 1027 | // 1028 | // ... type `bar` ... 1029 | // 1030 | 1031 | /* 1032 | text = text.replace(/ 1033 | (^|[^\\]) // Character before opening ` can't be a backslash 1034 | (`+) // $2 = Opening run of ` 1035 | ( // $3 = The code block 1036 | [^\r]*? 1037 | [^`] // attacklab: work around lack of lookbehind 1038 | ) 1039 | \2 // Matching closer 1040 | (?!`) 1041 | /gm, function(){...}); 1042 | */ 1043 | 1044 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 1045 | function (wholeMatch, m1, m2, m3, m4) { 1046 | var c = m3; 1047 | c = c.replace(/^([ \t]*)/g, ""); // leading whitespace 1048 | c = c.replace(/[ \t]*$/g, ""); // trailing whitespace 1049 | c = _EncodeCode(c); 1050 | 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. 1051 | return m1 + "" + c + ""; 1052 | } 1053 | ); 1054 | 1055 | return text; 1056 | } 1057 | 1058 | function _EncodeCode(text) { 1059 | // 1060 | // Encode/escape certain characters inside Markdown code runs. 1061 | // The point is that in code, these characters are literals, 1062 | // and lose their special Markdown meanings. 1063 | // 1064 | // Encode all ampersands; HTML entities are not 1065 | // entities within a Markdown code span. 1066 | text = text.replace(/&/g, "&"); 1067 | 1068 | // Do the angle bracket song and dance: 1069 | text = text.replace(//g, ">"); 1071 | 1072 | // Now, escape characters that are magic in Markdown: 1073 | text = escapeCharacters(text, "\*_{}[]\\", false); 1074 | 1075 | // jj the line above breaks this: 1076 | //--- 1077 | 1078 | //* Item 1079 | 1080 | // 1. Subitem 1081 | 1082 | // special char: * 1083 | //--- 1084 | 1085 | return text; 1086 | } 1087 | 1088 | function _DoItalicsAndBold(text) { 1089 | 1090 | // must go first: 1091 | text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g, 1092 | "$1$3$4"); 1093 | 1094 | text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g, 1095 | "$1$3$4"); 1096 | 1097 | return text; 1098 | } 1099 | 1100 | function _DoBlockQuotes(text) { 1101 | 1102 | /* 1103 | text = text.replace(/ 1104 | ( // Wrap whole match in $1 1105 | ( 1106 | ^[ \t]*>[ \t]? // '>' at the start of a line 1107 | .+\n // rest of the first line 1108 | (.+\n)* // subsequent consecutive lines 1109 | \n* // blanks 1110 | )+ 1111 | ) 1112 | /gm, function(){...}); 1113 | */ 1114 | 1115 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 1116 | function (wholeMatch, m1) { 1117 | var bq = m1; 1118 | 1119 | // attacklab: hack around Konqueror 3.5.4 bug: 1120 | // "----------bug".replace(/^-/g,"") == "bug" 1121 | 1122 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting 1123 | 1124 | // attacklab: clean up hack 1125 | bq = bq.replace(/~0/g, ""); 1126 | 1127 | bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines 1128 | bq = _RunBlockGamut(bq); // recurse 1129 | 1130 | bq = bq.replace(/(^|\n)/g, "$1 "); 1131 | // These leading spaces screw with
     content, so we need to fix that:
    1132 |                     bq = bq.replace(
    1133 |                             /(\s*
    [^\r]+?<\/pre>)/gm,
    1134 |                         function (wholeMatch, m1) {
    1135 |                             var pre = m1;
    1136 |                             // attacklab: hack around Konqueror 3.5.4 bug:
    1137 |                             pre = pre.replace(/^  /mg, "~0");
    1138 |                             pre = pre.replace(/~0/g, "");
    1139 |                             return pre;
    1140 |                         });
    1141 | 
    1142 |                     return hashBlock("
    \n" + bq + "\n
    "); 1143 | } 1144 | ); 1145 | return text; 1146 | } 1147 | 1148 | function _FormParagraphs(text, doNotUnhash) { 1149 | // 1150 | // Params: 1151 | // $text - string to process with html

    tags 1152 | // 1153 | 1154 | // Strip leading and trailing lines: 1155 | text = text.replace(/^\n+/g, ""); 1156 | text = text.replace(/\n+$/g, ""); 1157 | 1158 | var grafs = text.split(/\n{2,}/g); 1159 | var grafsOut = []; 1160 | 1161 | var markerRe = /~K(\d+)K/; 1162 | 1163 | // 1164 | // Wrap

    tags. 1165 | // 1166 | var end = grafs.length; 1167 | for (var i = 0; i < end; i++) { 1168 | var str = grafs[i]; 1169 | 1170 | // if this is an HTML marker, copy it 1171 | if (markerRe.test(str)) { 1172 | grafsOut.push(str); 1173 | } 1174 | else if (/\S/.test(str)) { 1175 | str = _RunSpanGamut(str); 1176 | str = str.replace(/^([ \t]*)/g, "

    "); 1177 | str += "

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

    tag here so Pagedown won't. 265 | Markdown.Extra.prototype.hashExtraBlock = function(block) { 266 | return '\n

    ~X' + (this.hashBlocks.push(block) - 1) + 'X

    \n'; 267 | }; 268 | Markdown.Extra.prototype.hashExtraInline = function(block) { 269 | return '~X' + (this.hashBlocks.push(block) - 1) + 'X'; 270 | }; 271 | 272 | // Replace placeholder blocks in `text` with their corresponding 273 | // html blocks in the hashBlocks array. 274 | Markdown.Extra.prototype.unHashExtraBlocks = function(text) { 275 | var self = this; 276 | function recursiveUnHash() { 277 | var hasHash = false; 278 | text = text.replace(/(?:

    )?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) { 279 | hasHash = true; 280 | var key = parseInt(m1, 10); 281 | return self.hashBlocks[key]; 282 | }); 283 | if(hasHash === true) { 284 | recursiveUnHash(); 285 | } 286 | } 287 | recursiveUnHash(); 288 | return text; 289 | }; 290 | 291 | // Wrap headers to make sure they won't be in def lists 292 | Markdown.Extra.prototype.wrapHeaders = function(text) { 293 | function wrap(text) { 294 | return '\n' + text + '\n'; 295 | } 296 | text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap); 297 | text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap); 298 | text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap); 299 | return text; 300 | }; 301 | 302 | 303 | /****************************************************************** 304 | * Attribute Blocks * 305 | *****************************************************************/ 306 | 307 | // TODO: use sentinels. Should we just add/remove them in doConversion? 308 | // TODO: better matches for id / class attributes 309 | var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}"; 310 | var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm"); 311 | var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + 312 | "(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead 313 | var fcbAttributes = new RegExp("^(```[ \\t]*[^{\\s]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + 314 | "(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm"); 315 | 316 | // Extract headers attribute blocks, move them above the element they will be 317 | // applied to, and hash them for later. 318 | Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) { 319 | 320 | var self = this; 321 | function attributeCallback(wholeMatch, pre, attr) { 322 | return '

    ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX

    \n' + pre + "\n"; 323 | } 324 | 325 | text = text.replace(hdrAttributesA, attributeCallback); // ## headers 326 | text = text.replace(hdrAttributesB, attributeCallback); // underline headers 327 | return text; 328 | }; 329 | 330 | // Extract FCB attribute blocks, move them above the element they will be 331 | // applied to, and hash them for later. 332 | Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) { 333 | // TODO: use sentinels. Should we just add/remove them in doConversion? 334 | // TODO: better matches for id / class attributes 335 | 336 | var self = this; 337 | function attributeCallback(wholeMatch, pre, attr) { 338 | return '

    ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX

    \n' + pre + "\n"; 339 | } 340 | 341 | return text.replace(fcbAttributes, attributeCallback); 342 | }; 343 | 344 | Markdown.Extra.prototype.applyAttributeBlocks = function(text) { 345 | var self = this; 346 | var blockRe = new RegExp('

    ~XX(\\d+)XX

    [\\s]*' + 347 | '(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?))', "gm"); 348 | text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) { 349 | if (!tag) // no following header or fenced code block. 350 | return ''; 351 | 352 | // get attributes list from hash 353 | var key = parseInt(k, 10); 354 | var attributes = self.hashBlocks[key]; 355 | 356 | // get id 357 | var id = attributes.match(/#[^\s#.]+/g) || []; 358 | var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : ''; 359 | 360 | // get classes and merge with existing classes 361 | var classes = attributes.match(/\.[^\s#.]+/g) || []; 362 | for (var i = 0; i < classes.length; i++) // Remove leading dot 363 | classes[i] = classes[i].substr(1, classes[i].length - 1); 364 | 365 | var classStr = ''; 366 | if (cls) 367 | classes = union(classes, [cls]); 368 | 369 | if (classes.length > 0) 370 | classStr = ' class="' + classes.join(' ') + '"'; 371 | 372 | return "<" + tag + idStr + classStr + rest; 373 | }); 374 | 375 | return text; 376 | }; 377 | 378 | /****************************************************************** 379 | * Tables * 380 | *****************************************************************/ 381 | 382 | // Find and convert Markdown Extra tables into html. 383 | Markdown.Extra.prototype.tables = function(text) { 384 | 385 | var self = this; 386 | 387 | var leadingPipe = new RegExp( 388 | ['^' , 389 | '[ ]{0,3}' , // Allowed whitespace 390 | '[|]' , // Initial pipe 391 | '(.+)\\n' , // $1: Header Row 392 | 393 | '[ ]{0,3}' , // Allowed whitespace 394 | '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator 395 | 396 | '(' , // $3: Table Body 397 | '(?:[ ]*[|].*\\n?)*' , // Table rows 398 | ')', 399 | '(?:\\n|$)' // Stop at final newline 400 | ].join(''), 401 | 'gm' 402 | ); 403 | 404 | var noLeadingPipe = new RegExp( 405 | ['^' , 406 | '[ ]{0,3}' , // Allowed whitespace 407 | '(\\S.*[|].*)\\n' , // $1: Header Row 408 | 409 | '[ ]{0,3}' , // Allowed whitespace 410 | '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator 411 | 412 | '(' , // $3: Table Body 413 | '(?:.*[|].*\\n?)*' , // Table rows 414 | ')' , 415 | '(?:\\n|$)' // Stop at final newline 416 | ].join(''), 417 | 'gm' 418 | ); 419 | 420 | text = text.replace(leadingPipe, doTable); 421 | text = text.replace(noLeadingPipe, doTable); 422 | 423 | // $1 = header, $2 = separator, $3 = body 424 | function doTable(match, header, separator, body, offset, string) { 425 | // remove any leading pipes and whitespace 426 | header = header.replace(/^ *[|]/m, ''); 427 | separator = separator.replace(/^ *[|]/m, ''); 428 | body = body.replace(/^ *[|]/gm, ''); 429 | 430 | // remove trailing pipes and whitespace 431 | header = header.replace(/[|] *$/m, ''); 432 | separator = separator.replace(/[|] *$/m, ''); 433 | body = body.replace(/[|] *$/gm, ''); 434 | 435 | // determine column alignments 436 | alignspecs = separator.split(/ *[|] */); 437 | align = []; 438 | for (var i = 0; i < alignspecs.length; i++) { 439 | var spec = alignspecs[i]; 440 | if (spec.match(/^ *-+: *$/m)) 441 | align[i] = ' style="text-align:right;"'; 442 | else if (spec.match(/^ *:-+: *$/m)) 443 | align[i] = ' style="text-align:center;"'; 444 | else if (spec.match(/^ *:-+ *$/m)) 445 | align[i] = ' style="text-align:left;"'; 446 | else align[i] = ''; 447 | } 448 | 449 | // TODO: parse spans in header and rows before splitting, so that pipes 450 | // inside of tags are not interpreted as separators 451 | var headers = header.split(/ *[|] */); 452 | var colCount = headers.length; 453 | 454 | // build html 455 | var cls = self.tableClass ? ' class="' + self.tableClass + '"' : ''; 456 | var html = ['\n', '\n', '\n'].join(''); 457 | 458 | // build column headers. 459 | for (i = 0; i < colCount; i++) { 460 | var headerHtml = convertSpans(trim(headers[i]), self); 461 | html += [" ", headerHtml, "\n"].join(''); 462 | } 463 | html += "\n\n"; 464 | 465 | // build rows 466 | var rows = body.split('\n'); 467 | for (i = 0; i < rows.length; i++) { 468 | if (rows[i].match(/^\s*$/)) // can apply to final row 469 | continue; 470 | 471 | // ensure number of rowCells matches colCount 472 | var rowCells = rows[i].split(/ *[|] */); 473 | var lenDiff = colCount - rowCells.length; 474 | for (var j = 0; j < lenDiff; j++) 475 | rowCells.push(''); 476 | 477 | html += "\n"; 478 | for (j = 0; j < colCount; j++) { 479 | var colHtml = convertSpans(trim(rowCells[j]), self); 480 | html += [" ", colHtml, "\n"].join(''); 481 | } 482 | html += "\n"; 483 | } 484 | 485 | html += "\n"; 486 | 487 | // replace html with placeholder until postConversion step 488 | return self.hashExtraBlock(html); 489 | } 490 | 491 | return text; 492 | }; 493 | 494 | 495 | /****************************************************************** 496 | * Footnotes * 497 | *****************************************************************/ 498 | 499 | // Strip footnote, store in hashes. 500 | Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) { 501 | var self = this; 502 | 503 | text = text.replace( 504 | /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g, 505 | function(wholeMatch, m1, m2) { 506 | m1 = slugify(m1); 507 | m2 += "\n"; 508 | m2 = m2.replace(/^[ ]{0,3}/g, ""); 509 | self.footnotes[m1] = m2; 510 | return "\n"; 511 | }); 512 | 513 | return text; 514 | }; 515 | 516 | 517 | // Find and convert footnotes references. 518 | Markdown.Extra.prototype.doFootnotes = function(text) { 519 | var self = this; 520 | if(self.isConvertingFootnote === true) { 521 | return text; 522 | } 523 | 524 | var footnoteCounter = 0; 525 | text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) { 526 | var id = slugify(m1); 527 | var footnote = self.footnotes[id]; 528 | if (footnote === undefined) { 529 | return wholeMatch; 530 | } 531 | footnoteCounter++; 532 | self.usedFootnotes.push(id); 533 | var html = '' + footnoteCounter 535 | + ''; 536 | return self.hashExtraInline(html); 537 | }); 538 | 539 | return text; 540 | }; 541 | 542 | // Print footnotes at the end of the document 543 | Markdown.Extra.prototype.printFootnotes = function(text) { 544 | var self = this; 545 | 546 | if (self.usedFootnotes.length === 0) { 547 | return text; 548 | } 549 | 550 | text += '\n\n
    \n
    \n
      \n\n'; 551 | for(var i=0; i' 560 | + formattedfootnote 561 | + ' \n\n'; 564 | } 565 | text += '
    \n
    '; 566 | return text; 567 | }; 568 | 569 | 570 | /****************************************************************** 571 | * Fenced Code Blocks (gfm) * 572 | ******************************************************************/ 573 | 574 | // Find and convert gfm-inspired fenced code blocks into html. 575 | Markdown.Extra.prototype.fencedCodeBlocks = function(text) { 576 | function encodeCode(code) { 577 | code = code.replace(/&/g, "&"); 578 | code = code.replace(//g, ">"); 580 | // These were escaped by PageDown before postNormalization 581 | code = code.replace(/~D/g, "$$"); 582 | code = code.replace(/~T/g, "~"); 583 | return code; 584 | } 585 | 586 | var self = this; 587 | text = text.replace(/(?:^|\n)```[ \t]*(\S*)[ \t]*\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) { 588 | var language = m1, codeblock = m2; 589 | 590 | // adhere to specified options 591 | var preclass = self.googleCodePrettify ? ' class="prettyprint"' : ''; 592 | var codeclass = ''; 593 | if (language) { 594 | if (self.googleCodePrettify || self.highlightJs) { 595 | // use html5 language- class names. supported by both prettify and highlight.js 596 | codeclass = ' class="language-' + language + '"'; 597 | } else { 598 | codeclass = ' class="' + language + '"'; 599 | } 600 | } 601 | 602 | var html = ['', 603 | encodeCode(codeblock), '
    '].join(''); 604 | 605 | // replace codeblock with placeholder until postConversion step 606 | return self.hashExtraBlock(html); 607 | }); 608 | 609 | return text; 610 | }; 611 | 612 | 613 | /****************************************************************** 614 | * SmartyPants * 615 | ******************************************************************/ 616 | 617 | Markdown.Extra.prototype.educatePants = function(text) { 618 | var self = this; 619 | var result = ''; 620 | var blockOffset = 0; 621 | // Here we parse HTML in a very bad manner 622 | text.replace(/(?:)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) { 623 | var token = text.substring(blockOffset, offset); 624 | result += self.applyPants(token); 625 | self.smartyPantsLastChar = result.substring(result.length - 1); 626 | blockOffset = offset + wholeMatch.length; 627 | if(!m1) { 628 | // Skip commentary 629 | result += wholeMatch; 630 | return; 631 | } 632 | // Skip special tags 633 | if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) { 634 | m4 = self.educatePants(m4); 635 | } 636 | else { 637 | self.smartyPantsLastChar = m4.substring(m4.length - 1); 638 | } 639 | result += m1 + m2 + m3 + m4 + m5; 640 | }); 641 | var lastToken = text.substring(blockOffset); 642 | result += self.applyPants(lastToken); 643 | self.smartyPantsLastChar = result.substring(result.length - 1); 644 | return result; 645 | }; 646 | 647 | function revertPants(wholeMatch, m1) { 648 | var blockText = m1; 649 | blockText = blockText.replace(/&\#8220;/g, "\""); 650 | blockText = blockText.replace(/&\#8221;/g, "\""); 651 | blockText = blockText.replace(/&\#8216;/g, "'"); 652 | blockText = blockText.replace(/&\#8217;/g, "'"); 653 | blockText = blockText.replace(/&\#8212;/g, "---"); 654 | blockText = blockText.replace(/&\#8211;/g, "--"); 655 | blockText = blockText.replace(/&\#8230;/g, "..."); 656 | return blockText; 657 | } 658 | 659 | Markdown.Extra.prototype.applyPants = function(text) { 660 | // Dashes 661 | text = text.replace(/---/g, "—").replace(/--/g, "–"); 662 | // Ellipses 663 | text = text.replace(/\.\.\./g, "…").replace(/\.\s\.\s\./g, "…"); 664 | // Backticks 665 | text = text.replace(/``/g, "“").replace (/''/g, "”"); 666 | 667 | if(/^'$/.test(text)) { 668 | // Special case: single-character ' token 669 | if(/\S/.test(this.smartyPantsLastChar)) { 670 | return "’"; 671 | } 672 | return "‘"; 673 | } 674 | if(/^"$/.test(text)) { 675 | // Special case: single-character " token 676 | if(/\S/.test(this.smartyPantsLastChar)) { 677 | return "”"; 678 | } 679 | return "“"; 680 | } 681 | 682 | // Special case if the very first character is a quote 683 | // followed by punctuation at a non-word-break. Close the quotes by brute force: 684 | text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "’"); 685 | text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "”"); 686 | 687 | // Special case for double sets of quotes, e.g.: 688 | //

    He said, "'Quoted' words in a larger quote."

    689 | text = text.replace(/"'(?=\w)/g, "“‘"); 690 | text = text.replace(/'"(?=\w)/g, "‘“"); 691 | 692 | // Special case for decade abbreviations (the '80s): 693 | text = text.replace(/'(?=\d{2}s)/g, "’"); 694 | 695 | // Get most opening single quotes: 696 | text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1‘"); 697 | 698 | // Single closing quotes: 699 | text = text.replace(/([^\s\[\{\(\-])'/g, "$1’"); 700 | text = text.replace(/'(?=\s|s\b)/g, "’"); 701 | 702 | // Any remaining single quotes should be opening ones: 703 | text = text.replace(/'/g, "‘"); 704 | 705 | // Get most opening double quotes: 706 | text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1“"); 707 | 708 | // Double closing quotes: 709 | text = text.replace(/([^\s\[\{\(\-])"/g, "$1”"); 710 | text = text.replace(/"(?=\s)/g, "”"); 711 | 712 | // Any remaining quotes should be opening ones. 713 | text = text.replace(/"/ig, "“"); 714 | return text; 715 | }; 716 | 717 | // Find and convert markdown extra definition lists into html. 718 | Markdown.Extra.prototype.runSmartyPants = function(text) { 719 | this.smartyPantsLastChar = ''; 720 | text = this.educatePants(text); 721 | // Clean everything inside html tags (some of them may have been converted due to our rough html parsing) 722 | text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants); 723 | return text; 724 | }; 725 | 726 | /****************************************************************** 727 | * Definition Lists * 728 | ******************************************************************/ 729 | 730 | // Find and convert markdown extra definition lists into html. 731 | Markdown.Extra.prototype.definitionLists = function(text) { 732 | var wholeList = new RegExp( 733 | ['(\\x02\\n?|\\n\\n)' , 734 | '(?:' , 735 | '(' , // $1 = whole list 736 | '(' , // $2 737 | '[ ]{0,3}' , 738 | '((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term 739 | '\\n?' , 740 | '[ ]{0,3}:[ ]+' , // colon starting definition 741 | ')' , 742 | '([\\s\\S]+?)' , 743 | '(' , // $4 744 | '(?=\\0x03)' , // \z 745 | '|' , 746 | '(?=' , 747 | '\\n{2,}' , 748 | '(?=\\S)' , 749 | '(?!' , // Negative lookahead for another term 750 | '[ ]{0,3}' , 751 | '(?:\\S.*\\n)+?' , // defined term 752 | '\\n?' , 753 | '[ ]{0,3}:[ ]+' , // colon starting definition 754 | ')' , 755 | '(?!' , // Negative lookahead for another definition 756 | '[ ]{0,3}:[ ]+' , // colon starting definition 757 | ')' , 758 | ')' , 759 | ')' , 760 | ')' , 761 | ')' 762 | ].join(''), 763 | 'gm' 764 | ); 765 | 766 | var self = this; 767 | text = addAnchors(text); 768 | 769 | text = text.replace(wholeList, function(match, pre, list) { 770 | var result = trim(self.processDefListItems(list)); 771 | result = "
    \n" + result + "\n
    "; 772 | return pre + self.hashExtraBlock(result) + "\n\n"; 773 | }); 774 | 775 | return removeAnchors(text); 776 | }; 777 | 778 | // Process the contents of a single definition list, splitting it 779 | // into individual term and definition list items. 780 | Markdown.Extra.prototype.processDefListItems = function(listStr) { 781 | var self = this; 782 | 783 | var dt = new RegExp( 784 | ['(\\x02\\n?|\\n\\n+)' , // leading line 785 | '(' , // definition terms = $1 786 | '[ ]{0,3}' , // leading whitespace 787 | '(?![:][ ]|[ ])' , // negative lookahead for a definition 788 | // mark (colon) or more whitespace 789 | '(?:\\S.*\\n)+?' , // actual term (not whitespace) 790 | ')' , 791 | '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed 792 | ].join(''), // with a definition mark 793 | 'gm' 794 | ); 795 | 796 | var dd = new RegExp( 797 | ['\\n(\\n+)?' , // leading line = $1 798 | '(' , // marker space = $2 799 | '[ ]{0,3}' , // whitespace before colon 800 | '[:][ ]+' , // definition mark (colon) 801 | ')' , 802 | '([\\s\\S]+?)' , // definition text = $3 803 | '(?=\\n*' , // stop at next definition mark, 804 | '(?:' , // next term or end of text 805 | '\\n[ ]{0,3}[:][ ]|' , 806 | '
    |\\x03' , // \z 807 | ')' , 808 | ')' 809 | ].join(''), 810 | 'gm' 811 | ); 812 | 813 | listStr = addAnchors(listStr); 814 | // trim trailing blank lines: 815 | listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n"); 816 | 817 | // Process definition terms. 818 | listStr = listStr.replace(dt, function(match, pre, termsStr) { 819 | var terms = trim(termsStr).split("\n"); 820 | var text = ''; 821 | for (var i = 0; i < terms.length; i++) { 822 | var term = terms[i]; 823 | // process spans inside dt 824 | term = convertSpans(trim(term), self); 825 | text += "\n
    " + term + "
    "; 826 | } 827 | return text + "\n"; 828 | }); 829 | 830 | // Process actual definitions. 831 | listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) { 832 | if (leadingLine || def.match(/\n{2,}/)) { 833 | // replace marker with the appropriate whitespace indentation 834 | def = Array(markerSpace.length + 1).join(' ') + def; 835 | // process markdown inside definition 836 | // TODO?: currently doesn't apply extensions 837 | def = outdent(def) + "\n\n"; 838 | def = "\n" + convertAll(def, self) + "\n"; 839 | } else { 840 | // convert span-level markdown inside definition 841 | def = rtrim(def); 842 | def = convertSpans(outdent(def), self); 843 | } 844 | 845 | return "\n
    " + def + "
    \n"; 846 | }); 847 | 848 | return removeAnchors(listStr); 849 | }; 850 | 851 | 852 | /*********************************************************** 853 | * Strikethrough * 854 | ************************************************************/ 855 | 856 | Markdown.Extra.prototype.strikethrough = function(text) { 857 | // Pretty much duplicated from _DoItalicsAndBold 858 | return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g, 859 | "$1$2$3"); 860 | }; 861 | 862 | 863 | /*********************************************************** 864 | * New lines * 865 | ************************************************************/ 866 | 867 | Markdown.Extra.prototype.newlines = function(text) { 868 | // We have to ignore already converted newlines and line breaks in sub-list items 869 | return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) { 870 | return previousTag ? wholeMatch : "
    \n"; 871 | }); 872 | }; 873 | 874 | })(); 875 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/markdown.sanitizer.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var output, Converter; 3 | if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module 4 | output = exports; 5 | Converter = require("./Markdown.Converter").Converter; 6 | } else { 7 | output = window.Markdown; 8 | Converter = output.Converter; 9 | } 10 | 11 | output.getSanitizingConverter = function () { 12 | var converter = new Converter(); 13 | converter.hooks.chain("postConversion", sanitizeHtml); 14 | converter.hooks.chain("postConversion", balanceTags); 15 | return converter; 16 | } 17 | 18 | function sanitizeHtml(html) { 19 | return html.replace(/<[^>]*>?/gi, sanitizeTag); 20 | } 21 | 22 | // (tags that can be opened/closed) | (tags that stand alone) 23 | var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i; 24 | // | 25 | var a_white = /^(]+")?\s?>|<\/a>)$/i; 26 | 27 | // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i; 29 | 30 | //
    |
    for twitter bootstrap 31 | var pre_white = /^(|<\/pre>)$/i; 32 | 33 | function sanitizeTag(tag) { 34 | if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white)) 35 | return tag; 36 | else 37 | return ""; 38 | } 39 | 40 | /// 41 | /// attempt to balance HTML tags in the html string 42 | /// by removing any unmatched opening or closing tags 43 | /// IMPORTANT: we *assume* HTML has *already* been 44 | /// sanitized and is safe/sane before balancing! 45 | /// 46 | /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593 47 | /// 48 | function balanceTags(html) { 49 | 50 | if (html == "") 51 | return ""; 52 | 53 | var re = /<\/?\w+[^>]*(\s|$|>)/g; 54 | // convert everything to lower case; this makes 55 | // our case insensitive comparisons easier 56 | var tags = html.toLowerCase().match(re); 57 | 58 | // no HTML tags present? nothing to do; exit now 59 | var tagcount = (tags || []).length; 60 | if (tagcount == 0) 61 | return html; 62 | 63 | var tagname, tag; 64 | var ignoredtags = "



  • "; 65 | var match; 66 | var tagpaired = []; 67 | var tagremove = []; 68 | var needsRemoval = false; 69 | 70 | // loop through matched tags in forward order 71 | for (var ctag = 0; ctag < tagcount; ctag++) { 72 | tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1"); 73 | // skip any already paired tags 74 | // and skip tags in our ignore list; assume they're self-closed 75 | if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1) 76 | continue; 77 | 78 | tag = tags[ctag]; 79 | match = -1; 80 | 81 | if (!/^<\//.test(tag)) { 82 | // this is an opening tag 83 | // search forwards (next tags), look for closing tags 84 | for (var ntag = ctag + 1; ntag < tagcount; ntag++) { 85 | if (!tagpaired[ntag] && tags[ntag] == "") { 86 | match = ntag; 87 | break; 88 | } 89 | } 90 | } 91 | 92 | if (match == -1) 93 | needsRemoval = tagremove[ctag] = true; // mark for removal 94 | else 95 | tagpaired[match] = true; // mark paired 96 | } 97 | 98 | if (!needsRemoval) 99 | return html; 100 | 101 | // delete all orphaned tags from the string 102 | 103 | var ctag = 0; 104 | html = html.replace(re, function (match) { 105 | var res = tagremove[ctag] ? "" : match; 106 | ctag++; 107 | return res; 108 | }); 109 | return html; 110 | } 111 | })(); 112 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/pagedown_bootstrap.js: -------------------------------------------------------------------------------- 1 | //= require markdown.converter 2 | //= require markdown.editor 3 | //= require markdown.sanitizer 4 | //= require markdown.extra 5 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/pagedown_init.js.coffee.erb: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('textarea.wmd-input').each (i, input) -> 3 | attr = $(input).attr('id').split('wmd-input')[1] 4 | converter = new Markdown.Converter() 5 | Markdown.Extra.init(converter) 6 | help = 7 | handler: () -> 8 | window.open('http://daringfireball.net/projects/markdown/syntax') 9 | return false 10 | title: "<%= I18n.t('components.markdown_editor.help', default: 'Markdown Editing Help') %>" 11 | editor = new Markdown.Editor(converter, attr, help) 12 | editor.run() -------------------------------------------------------------------------------- /vendor/assets/stylesheets/pagedown_bootstrap.scss: -------------------------------------------------------------------------------- 1 | $baseFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif !default; 2 | $baseFontSize: 14px !default; 3 | 4 | .wmd-input { 5 | margin-top: 2px; 6 | margin-bottom: 10px; 7 | height: 500px; 8 | width: 100%; 9 | box-sizing: border-box; 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | -ms-box-sizing: border-box; 13 | &.wmd-input-small{ 14 | height: 50px; 15 | } 16 | } 17 | 18 | .wmd-preview { 19 | min-height: 20px; 20 | max-height: 500px; 21 | overflow-y: scroll; 22 | padding: 19px; 23 | margin: 32px 0 0; 24 | background-color: #f5f5f5; 25 | border: 1px solid #eee; 26 | border: 1px solid rgba(0, 0, 0, 0.05); 27 | -webkit-border-radius: 4px; 28 | -moz-border-radius: 4px; 29 | border-radius: 4px; 30 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); 31 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); 32 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); 33 | width: 100%; 34 | box-sizing: border-box; 35 | -webkit-box-sizing: border-box; 36 | -moz-box-sizing: border-box; 37 | -ms-box-sizing: border-box; 38 | } 39 | 40 | .wmd-preview blockquote { 41 | border-color: #ddd; 42 | border-color: rgba(0, 0, 0, 0.15); 43 | } 44 | 45 | .wmd-panel .btn-toolbar { 46 | margin-bottom: 0; 47 | padding: 0; 48 | width: 100%; 49 | } 50 | 51 | .wmd-prompt-background { 52 | background-color: #000000; 53 | } 54 | 55 | .wmd-prompt-dialog { 56 | border: 1px solid #999999; 57 | background-color: #F5F5F5; 58 | } 59 | 60 | .wmd-prompt-dialog > div { 61 | font-size: $baseFontSize; 62 | font-family: $baseFontFamily; 63 | } 64 | 65 | .wmd-prompt-dialog > form > input[type="text"] { 66 | border: 1px solid #999999; 67 | color: black; 68 | } 69 | 70 | .wmd-prompt-dialog > form > input[type="button"] { 71 | border: 1px solid #888888; 72 | font-family: $baseFontFamily; 73 | font-size: $baseFontSize; 74 | font-weight: bold; 75 | } --------------------------------------------------------------------------------