├── .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 |  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 | // ands 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 | //
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 ` Just type 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 += " tag here so Pagedown won't.
265 | Markdown.Extra.prototype.hashExtraBlock = function(block) {
266 | return '\n ~X' + (this.hashBlocks.push(block) - 1) + 'X )?~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 ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX ~XX(\\d+)XX He said, "'Quoted' words in a larger quote."
\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 | ( // href = $4
551 | (?:
552 | \([^)]*\) // allow one level of (correctly nested) parens (think MSDN)
553 | |
554 | [^()\s]
555 | )*?
556 | )>?
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]*()((?:\([^)]*\)|[^()\s])*?)>?[ \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 = "" + link_text + "";
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: 
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 | (\S+?)>? // 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]*()(\S+?)>?[ \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(/";
743 |
744 | return result;
745 | }
746 |
747 | function _DoHeaders(text) {
748 |
749 | // Setext-style headers:
750 | // Header 1
751 | // ========
752 | //
753 | // Header 2
754 | // --------
755 | //
756 | text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
757 | function (wholeMatch, m1) { return "
" + _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 "` 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 = "
";
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 " + codeblock + "\n
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 | //
foo `bar` baz
at the prompt.`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 '+title+'
';
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]*(\S+?)>?[ \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 \n', '\n', '
\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'].join('');
457 |
458 | // build column headers.
459 | for (i = 0; i < colCount; i++) {
460 | var headerHtml = convertSpans(trim(headers[i]), self);
461 | 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 += "", headerHtml, " \n"].join('');
462 | }
463 | html += "\n";
478 | for (j = 0; j < colCount; j++) {
479 | var colHtml = convertSpans(trim(rowCells[j]), self);
480 | html += [" \n";
483 | }
484 |
485 | html += "", colHtml, " \n"].join('');
481 | }
482 | html += "
\n\n\n';
551 | for(var i=0; i
\n
'].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 | // ',
603 | encodeCode(codeblock), '
\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 | '$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 | ///
";
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] == "" + tagname + ">") {
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 | }
--------------------------------------------------------------------------------