s around 364 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 365 | // phrase emphasis, and spans. The list of tags we're looking for is 366 | // hard-coded: 367 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" 368 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" 369 | 370 | // First, look for nested blocks, e.g.: 371 | //
tags around block-level tags.
518 | text = _HashHTMLBlocks(text);
519 |
520 | text = _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs);
521 |
522 | return text;
523 | }
524 |
525 | function _RunSpanGamut(text) {
526 | //
527 | // These are all the transformations that occur *within* block-level
528 | // tags like paragraphs, headers, and list items.
529 | //
530 |
531 | text = pluginHooks.preSpanGamut(text);
532 |
533 | text = _DoCodeSpans(text);
534 | text = _EscapeSpecialCharsWithinTagAttributes(text);
535 | text = _EncodeBackslashEscapes(text);
536 |
537 | // Process anchor and image tags. Images must come first,
538 | // because ![foo][f] looks like an anchor.
539 | text = _DoImages(text);
540 | text = _DoAnchors(text);
541 |
542 | // Make links out of things like ` Just type tags
1371 | //
1372 |
1373 | // Strip leading and trailing lines:
1374 | text = text.replace(/^\n+/g, "");
1375 | text = text.replace(/\n+$/g, "");
1376 |
1377 | var grafs = text.split(/\n{2,}/g);
1378 | var grafsOut = [];
1379 |
1380 | var markerRe = /~K(\d+)K/;
1381 |
1382 | //
1383 | // Wrap tags.
1384 | //
1385 | var end = grafs.length;
1386 | for (var i = 0; i < end; i++) {
1387 | var str = grafs[i];
1388 |
1389 | // if this is an HTML marker, copy it
1390 | if (markerRe.test(str)) {
1391 | grafsOut.push(str);
1392 | }
1393 | else if (/\S/.test(str)) {
1394 | str = _RunSpanGamut(str);
1395 | str = str.replace(/^([ \t]*)/g, doNotCreateParagraphs ? "" : " ");
1396 | if (!doNotCreateParagraphs)
1397 | str += " Insert Hyperlink http://example.com/ \"optional title\" Insert Image http://example.com/images/diagram.jpg \"optional title\" Insérer un hyperlien http://example.com/ \"titre optionnel\" Insérer une image http://example.com/images/diagram.jpg \"titre optionnel\"
\n");
554 |
555 | text = pluginHooks.postSpanGamut(text);
556 |
557 | return text;
558 | }
559 |
560 | function _EscapeSpecialCharsWithinTagAttributes(text) {
561 | //
562 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they
563 | // don't conflict with their use in Markdown for code, italics and strong.
564 | //
565 |
566 | // Build a regex to find HTML tags and comments. See Friedl's
567 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
568 |
569 | // SE: changed the comment part of the regex
570 |
571 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi;
572 |
573 | text = text.replace(regex, function (wholeMatch) {
574 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`");
575 | tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackexchange.com/questions/95987
576 | return tag;
577 | });
578 |
579 | return text;
580 | }
581 |
582 | function _DoAnchors(text) {
583 |
584 | if (text.indexOf("[") === -1)
585 | return text;
586 |
587 | //
588 | // Turn Markdown link shortcuts into XHTML tags.
589 | //
590 | //
591 | // First, handle reference-style links: [link text] [id]
592 | //
593 |
594 | /*
595 | text = text.replace(/
596 | ( // wrap whole match in $1
597 | \[
598 | (
599 | (?:
600 | \[[^\]]*\] // allow brackets nested one level
601 | |
602 | [^\[] // or anything else
603 | )*
604 | )
605 | \]
606 |
607 | [ ]? // one optional space
608 | (?:\n[ ]*)? // one optional newline followed by spaces
609 |
610 | \[
611 | (.*?) // id = $3
612 | \]
613 | )
614 | ()()()() // pad remaining backreferences
615 | /g, writeAnchorTag);
616 | */
617 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);
618 |
619 | //
620 | // Next, inline-style links: [link text](url "optional title")
621 | //
622 |
623 | /*
624 | text = text.replace(/
625 | ( // wrap whole match in $1
626 | \[
627 | (
628 | (?:
629 | \[[^\]]*\] // allow brackets nested one level
630 | |
631 | [^\[\]] // or anything else
632 | )*
633 | )
634 | \]
635 | \( // literal paren
636 | [ \t]*
637 | () // no id, so leave $3 empty
638 | ( // href = $4
639 | (?:
640 | \([^)]*\) // allow one level of (correctly nested) parens (think MSDN)
641 | |
642 | [^()\s]
643 | )*?
644 | )>?
645 | [ \t]*
646 | ( // $5
647 | (['"]) // quote char = $6
648 | (.*?) // Title = $7
649 | \6 // matching quote
650 | [ \t]* // ignore any spaces/tabs between closing quote and )
651 | )? // title is optional
652 | \)
653 | )
654 | /g, writeAnchorTag);
655 | */
656 |
657 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()((?:\([^)]*\)|[^()\s])*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag);
658 |
659 | //
660 | // Last, handle reference-style shortcuts: [link text]
661 | // These must come last in case you've also got [link test][1]
662 | // or [link test](/foo)
663 | //
664 |
665 | /*
666 | text = text.replace(/
667 | ( // wrap whole match in $1
668 | \[
669 | ([^\[\]]+) // link text = $2; can't contain '[' or ']'
670 | \]
671 | )
672 | ()()()()() // pad rest of backreferences
673 | /g, writeAnchorTag);
674 | */
675 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
676 |
677 | return text;
678 | }
679 |
680 | function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
681 | if (m7 == undefined) m7 = "";
682 | var whole_match = m1;
683 | var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs
684 | var link_id = m3.toLowerCase();
685 | var url = m4;
686 | var title = m7;
687 |
688 | if (url == "") {
689 | if (link_id == "") {
690 | // lower-case and turn embedded newlines into spaces
691 | link_id = link_text.toLowerCase().replace(/ ?\n/g, " ");
692 | }
693 | url = "#" + link_id;
694 |
695 | if (g_urls.get(link_id) != undefined) {
696 | url = g_urls.get(link_id);
697 | if (g_titles.get(link_id) != undefined) {
698 | title = g_titles.get(link_id);
699 | }
700 | }
701 | else {
702 | if (whole_match.search(/\(\s*\)$/m) > -1) {
703 | // Special case for explicit empty url
704 | url = "";
705 | } else {
706 | return whole_match;
707 | }
708 | }
709 | }
710 | url = attributeSafeUrl(url);
711 |
712 | var result = "" + link_text + "";
721 |
722 | return result;
723 | }
724 |
725 | function _DoImages(text) {
726 |
727 | if (text.indexOf("![") === -1)
728 | return text;
729 |
730 | //
731 | // Turn Markdown image shortcuts into tags.
732 | //
733 |
734 | //
735 | // First, handle reference-style labeled images: ![alt text][id]
736 | //
737 |
738 | /*
739 | text = text.replace(/
740 | ( // wrap whole match in $1
741 | !\[
742 | (.*?) // alt text = $2
743 | \]
744 |
745 | [ ]? // one optional space
746 | (?:\n[ ]*)? // one optional newline followed by spaces
747 |
748 | \[
749 | (.*?) // id = $3
750 | \]
751 | )
752 | ()()()() // pad rest of backreferences
753 | /g, writeImageTag);
754 | */
755 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag);
756 |
757 | //
758 | // Next, handle inline images: 
759 | // Don't forget: encode * and _
760 |
761 | /*
762 | text = text.replace(/
763 | ( // wrap whole match in $1
764 | !\[
765 | (.*?) // alt text = $2
766 | \]
767 | \s? // One optional whitespace character
768 | \( // literal paren
769 | [ \t]*
770 | () // no id, so leave $3 empty
771 | (\S+?)>? // src url = $4
772 | [ \t]*
773 | ( // $5
774 | (['"]) // quote char = $6
775 | (.*?) // title = $7
776 | \6 // matching quote
777 | [ \t]*
778 | )? // title is optional
779 | \)
780 | )
781 | /g, writeImageTag);
782 | */
783 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag);
784 |
785 | return text;
786 | }
787 |
788 | function attributeEncode(text) {
789 | // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title)
790 | // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it)
791 | return text.replace(/>/g, ">").replace(/";
835 |
836 | return result;
837 | }
838 |
839 | function _DoHeaders(text) {
840 |
841 | // Setext-style headers:
842 | // Header 1
843 | // ========
844 | //
845 | // Header 2
846 | // --------
847 | //
848 | text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
849 | function (wholeMatch, m1) { return "
" + _RunSpanGamut(m1) + "
\n\n"; }
850 | );
851 |
852 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
853 | function (matchFound, m1) { return "" + _RunSpanGamut(m1) + "
\n\n"; }
854 | );
855 |
856 | // atx-style headers:
857 | // # Header 1
858 | // ## Header 2
859 | // ## Header 2 with closing hashes ##
860 | // ...
861 | // ###### Header 6
862 | //
863 |
864 | /*
865 | text = text.replace(/
866 | ^(\#{1,6}) // $1 = string of #'s
867 | [ \t]*
868 | (.+?) // $2 = Header text
869 | [ \t]*
870 | \#* // optional closing #'s (not counted)
871 | \n+
872 | /gm, function() {...});
873 | */
874 |
875 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
876 | function (wholeMatch, m1, m2) {
877 | var h_level = m1.length;
878 | return "` blocks.
1059 | //
1060 |
1061 | /*
1062 | text = text.replace(/
1063 | (?:\n\n|^)
1064 | ( // $1 = the code block -- one or more lines, starting with a space/tab
1065 | (?:
1066 | (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
1067 | .*\n+
1068 | )+
1069 | )
1070 | (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
1071 | /g ,function(){...});
1072 | */
1073 |
1074 | // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
1075 | text += "~0";
1076 |
1077 | text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
1078 | function (wholeMatch, m1, m2) {
1079 | var codeblock = m1;
1080 | var nextChar = m2;
1081 |
1082 | codeblock = _EncodeCode(_Outdent(codeblock));
1083 | codeblock = _Detab(codeblock);
1084 | codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
1085 | codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
1086 |
1087 | codeblock = "
";
1088 |
1089 | return "\n\n" + codeblock + "\n\n" + nextChar;
1090 | }
1091 | );
1092 |
1093 | // attacklab: strip sentinel
1094 | text = text.replace(/~0/, "");
1095 |
1096 | return text;
1097 | }
1098 |
1099 | function _DoCodeSpans(text) {
1100 | //
1101 | // * Backtick quotes are used for " + codeblock + "\n
spans.
1102 | //
1103 | // * You can use multiple backticks as the delimiters if you want to
1104 | // include literal backticks in the code span. So, this input:
1105 | //
1106 | // Just type ``foo `bar` baz`` at the prompt.
1107 | //
1108 | // Will translate to:
1109 | //
1110 | //
foo `bar` baz
at the prompt.`bar`
...
1123 | //
1124 |
1125 | /*
1126 | text = text.replace(/
1127 | (^|[^\\`]) // Character before opening ` can't be a backslash or backtick
1128 | (`+) // $2 = Opening run of `
1129 | (?!`) // and no more backticks -- match the full run
1130 | ( // $3 = The code block
1131 | [^\r]*?
1132 | [^`] // attacklab: work around lack of lookbehind
1133 | )
1134 | \2 // Matching closer
1135 | (?!`)
1136 | /gm, function(){...});
1137 | */
1138 |
1139 | text = text.replace(/(^|[^\\`])(`+)(?!`)([^\r]*?[^`])\2(?!`)/gm,
1140 | function (wholeMatch, m1, m2, m3, m4) {
1141 | var c = m3;
1142 | c = c.replace(/^([ \t]*)/g, ""); // leading whitespace
1143 | c = c.replace(/[ \t]*$/g, ""); // trailing whitespace
1144 | c = _EncodeCode(c);
1145 | 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.
1146 | return m1 + "" + c + "
";
1147 | }
1148 | );
1149 |
1150 | return text;
1151 | }
1152 |
1153 | function _EncodeCode(text) {
1154 | //
1155 | // Encode/escape certain characters inside Markdown code runs.
1156 | // The point is that in code, these characters are literals,
1157 | // and lose their special Markdown meanings.
1158 | //
1159 | // Encode all ampersands; HTML entities are not
1160 | // entities within a Markdown code span.
1161 | text = text.replace(/&/g, "&");
1162 |
1163 | // Do the angle bracket song and dance:
1164 | text = text.replace(//g, ">");
1166 |
1167 | // Now, escape characters that are magic in Markdown:
1168 | text = escapeCharacters(text, "\*_{}[]\\", false);
1169 |
1170 | // jj the line above breaks this:
1171 | //---
1172 |
1173 | //* Item
1174 |
1175 | // 1. Subitem
1176 |
1177 | // special char: *
1178 | //---
1179 |
1180 | return text;
1181 | }
1182 |
1183 | function _DoItalicsAndBoldStrict(text) {
1184 |
1185 | if (text.indexOf("*") === -1 && text.indexOf("_") === - 1)
1186 | return text;
1187 |
1188 | text = asciify(text);
1189 |
1190 | // must go first:
1191 |
1192 | // (^|[\W_]) Start with a non-letter or beginning of string. Store in \1.
1193 | // (?:(?!\1)|(?=^)) Either the next character is *not* the same as the previous,
1194 | // or we started at the end of the string (in which case the previous
1195 | // group had zero width, so we're still there). Because the next
1196 | // character is the marker, this means that if there are e.g. multiple
1197 | // underscores in a row, we can only match the left-most ones (which
1198 | // prevents foo___bar__ from getting bolded)
1199 | // (\*|_) The marker character itself, asterisk or underscore. Store in \2.
1200 | // \2 The marker again, since bold needs two.
1201 | // (?=\S) The first bolded character cannot be a space.
1202 | // ([^\r]*?\S) The actual bolded string. At least one character, and it cannot *end*
1203 | // with a space either. Note that like in many other places, [^\r] is
1204 | // just a workaround for JS' lack of single-line regexes; it's equivalent
1205 | // to a . in an /s regex, because the string cannot contain any \r (they
1206 | // are removed in the normalizing step).
1207 | // \2\2 The marker character, twice -- end of bold.
1208 | // (?!\2) Not followed by another marker character (ensuring that we match the
1209 | // rightmost two in a longer row)...
1210 | // (?=[\W_]|$) ...but by any other non-word character or the end of string.
1211 | text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)([^\r]*?\S)\2\2(?!\2)(?=[\W_]|$)/g,
1212 | "$1$3");
1213 |
1214 | // This is almost identical to the regex, except 1) there's obviously just one marker
1215 | // character, and 2) the italicized string cannot contain the marker character.
1216 | text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2)[^\r])*?\S)\2(?!\2)(?=[\W_]|$)/g,
1217 | "$1$3");
1218 |
1219 | return deasciify(text);
1220 | }
1221 |
1222 | function _DoItalicsAndBold_AllowIntrawordWithAsterisk(text) {
1223 |
1224 | if (text.indexOf("*") === -1 && text.indexOf("_") === - 1)
1225 | return text;
1226 |
1227 | text = asciify(text);
1228 |
1229 | // must go first:
1230 | // (?=[^\r][*_]|[*_]) Optimization only, to find potentially relevant text portions faster. Minimally slower in Chrome, but much faster in IE.
1231 | // ( Store in \1. This is the last character before the delimiter
1232 | // ^ Either we're at the start of the string (i.e. there is no last character)...
1233 | // | ... or we allow one of the following:
1234 | // (?= (lookahead; we're not capturing this, just listing legal possibilities)
1235 | // \W__ If the delimiter is __, then this last character must be non-word non-underscore (extra-word emphasis only)
1236 | // |
1237 | // (?!\*)[\W_]\*\* If the delimiter is **, then this last character can be non-word non-asterisk (extra-word emphasis)...
1238 | // |
1239 | // \w\*\*\w ...or it can be word/underscore, but only if the first bolded character is such a character as well (intra-word emphasis)
1240 | // )
1241 | // [^\r] actually capture the character (can't use `.` since it could be \n)
1242 | // )
1243 | // (\*\*|__) Store in \2: the actual delimiter
1244 | // (?!\2) not followed by the delimiter again (at most one more asterisk/underscore is allowed)
1245 | // (?=\S) the first bolded character can't be a space
1246 | // ( Store in \3: the bolded string
1247 | //
1248 | // (?:| Look at all bolded characters except for the last one. Either that's empty, meaning only a single character was bolded...
1249 | // [^\r]*? ... otherwise take arbitrary characters, minimally matching; that's all bolded characters except for the last *two*
1250 | // (?!\2) the last two characters cannot be the delimiter itself (because that would mean four underscores/asterisks in a row)
1251 | // [^\r] capture the next-to-last bolded character
1252 | // )
1253 | // (?= lookahead at the very last bolded char and what comes after
1254 | // \S_ for underscore-bolding, it can be any non-space
1255 | // |
1256 | // \w for asterisk-bolding (otherwise the previous alternative would've matched, since \w implies \S), either the last char is word/underscore...
1257 | // |
1258 | // \S\*\*(?:[\W_]|$) ... or it's any other non-space, but in that case the character *after* the delimiter may not be a word character
1259 | // )
1260 | // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases)
1261 | // )
1262 | // (?= lookahead; list the legal possibilities for the closing delimiter and its following character
1263 | // __(?:\W|$) for underscore-bolding, the following character (if any) must be non-word non-underscore
1264 | // |
1265 | // \*\*(?:[^*]|$) for asterisk-bolding, any non-asterisk is allowed (note we already ensured above that it's not a word character if the last bolded character wasn't one)
1266 | // )
1267 | // \2 actually capture the closing delimiter (and make sure that it matches the opening one)
1268 |
1269 | text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W__|(?!\*)[\W_]\*\*|\w\*\*\w)[^\r])(\*\*|__)(?!\2)(?=\S)((?:|[^\r]*?(?!\2)[^\r])(?=\S_|\w|\S\*\*(?:[\W_]|$)).)(?=__(?:\W|$)|\*\*(?:[^*]|$))\2/g,
1270 | "$1$3");
1271 |
1272 | // now :
1273 | // (?=[^\r][*_]|[*_]) Optimization, see above.
1274 | // ( Store in \1. This is the last character before the delimiter
1275 | // ^ Either we're at the start of the string (i.e. there is no last character)...
1276 | // | ... or we allow one of the following:
1277 | // (?= (lookahead; we're not capturing this, just listing legal possibilities)
1278 | // \W_ If the delimiter is _, then this last character must be non-word non-underscore (extra-word emphasis only)
1279 | // |
1280 | // (?!\*) otherwise, we list two possiblities for * as the delimiter; in either case, the last characters cannot be an asterisk itself
1281 | // (?:
1282 | // [\W_]\* this last character can be non-word (extra-word emphasis)...
1283 | // |
1284 | // \D\*(?=\w)\D ...or it can be word (otherwise the first alternative would've matched), but only if
1285 | // a) the first italicized character is such a character as well (intra-word emphasis), and
1286 | // b) neither character on either side of the asterisk is a digit
1287 | // )
1288 | // )
1289 | // [^\r] actually capture the character (can't use `.` since it could be \n)
1290 | // )
1291 | // (\*|_) Store in \2: the actual delimiter
1292 | // (?!\2\2\2) not followed by more than two more instances of the delimiter
1293 | // (?=\S) the first italicized character can't be a space
1294 | // ( Store in \3: the italicized string
1295 | // (?:(?!\2)[^\r])*? arbitrary characters except for the delimiter itself, minimally matching
1296 | // (?= lookahead at the very last italicized char and what comes after
1297 | // [^\s_]_ for underscore-italicizing, it can be any non-space non-underscore
1298 | // |
1299 | // (?=\w)\D\*\D for asterisk-italicizing, either the last char is word/underscore *and* neither character on either side of the asterisk is a digit...
1300 | // |
1301 | // [^\s*]\*(?:[\W_]|$) ... or that last char is any other non-space non-asterisk, but then the character after the delimiter (if any) must be non-word
1302 | // )
1303 | // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases)
1304 | // )
1305 | // (?= lookahead; list the legal possibilities for the closing delimiter and its following character
1306 | // _(?:\W|$) for underscore-italicizing, the following character (if any) must be non-word non-underscore
1307 | // |
1308 | // \*(?:[^*]|$) for asterisk-italicizing, any non-asterisk is allowed; all other restrictions have already been ensured in the previous lookahead
1309 | // )
1310 | // \2 actually capture the closing delimiter (and make sure that it matches the opening one)
1311 |
1312 | text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W_|(?!\*)(?:[\W_]\*|\D\*(?=\w)\D))[^\r])(\*|_)(?!\2\2\2)(?=\S)((?:(?!\2)[^\r])*?(?=[^\s_]_|(?=\w)\D\*\D|[^\s*]\*(?:[\W_]|$)).)(?=_(?:\W|$)|\*(?:[^*]|$))\2/g,
1313 | "$1$3");
1314 |
1315 | return deasciify(text);
1316 | }
1317 |
1318 |
1319 | function _DoBlockQuotes(text) {
1320 |
1321 | /*
1322 | text = text.replace(/
1323 | ( // Wrap whole match in $1
1324 | (
1325 | ^[ \t]*>[ \t]? // '>' at the start of a line
1326 | .+\n // rest of the first line
1327 | (.+\n)* // subsequent consecutive lines
1328 | \n* // blanks
1329 | )+
1330 | )
1331 | /gm, function(){...});
1332 | */
1333 |
1334 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
1335 | function (wholeMatch, m1) {
1336 | var bq = m1;
1337 |
1338 | // attacklab: hack around Konqueror 3.5.4 bug:
1339 | // "----------bug".replace(/^-/g,"") == "bug"
1340 |
1341 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting
1342 |
1343 | // attacklab: clean up hack
1344 | bq = bq.replace(/~0/g, "");
1345 |
1346 | bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines
1347 | bq = _RunBlockGamut(bq); // recurse
1348 |
1349 | bq = bq.replace(/(^|\n)/g, "$1 ");
1350 | // These leading spaces screw with content, so we need to fix that:
1351 | bq = bq.replace(
1352 | /(\s*
[^\r]+?<\/pre>)/gm,
1353 | function (wholeMatch, m1) {
1354 | var pre = m1;
1355 | // attacklab: hack around Konqueror 3.5.4 bug:
1356 | pre = pre.replace(/^ /mg, "~0");
1357 | pre = pre.replace(/~0/g, "");
1358 | return pre;
1359 | });
1360 |
1361 | return hashBlock("
\n" + bq + "\n
");
1362 | }
1363 | );
1364 | return text;
1365 | }
1366 |
1367 | function _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs) {
1368 | //
1369 | // Params:
1370 | // $text - string to process with html Ctrl+Q",
32 | quoteexample: "Blockquote",
33 |
34 | code: "Code Sample
Ctrl+K",
35 | codeexample: "enter code here",
36 |
37 | image: "Image
Ctrl+G",
38 | imagedescription: "enter image description here",
39 | imagedialog: "
Need free image hosting? Ctrl+O",
42 | ulist: "Bulleted List
Ctrl+U",
43 | litem: "List item",
44 |
45 | heading: "Heading
/
Ctrl+H",
46 | headingexample: "Heading",
47 |
48 | hr: "Horizontal Rule
Ctrl+R",
49 |
50 | undo: "Undo - Ctrl+Z",
51 | redo: "Redo - Ctrl+Y",
52 | redomac: "Redo - Ctrl+Shift+Z",
53 |
54 | help: "Markdown Editing Help"
55 | };
56 |
57 |
58 | // -------------------------------------------------------------------
59 | // YOUR CHANGES GO HERE
60 | //
61 | // I've tried to localize the things you are likely to change to
62 | // this area.
63 | // -------------------------------------------------------------------
64 |
65 | // The default text that appears in the dialog input box when entering
66 | // links.
67 | var imageDefaultText = "http://";
68 | var linkDefaultText = "http://";
69 |
70 | // -------------------------------------------------------------------
71 | // END OF YOUR CHANGES
72 | // -------------------------------------------------------------------
73 |
74 | // options, if given, can have the following properties:
75 | // options.helpButton = { handler: yourEventHandler }
76 | // options.strings = { italicexample: "slanted text" }
77 | // `yourEventHandler` is the click handler for the help button.
78 | // If `options.helpButton` isn't given, not help button is created.
79 | // `options.strings` can have any or all of the same properties as
80 | // `defaultStrings` above, so you can just override some string displayed
81 | // to the user on a case-by-case basis, or translate all strings to
82 | // a different language.
83 | //
84 | // For backwards compatibility reasons, the `options` argument can also
85 | // be just the `helpButton` object, and `strings.help` can also be set via
86 | // `helpButton.title`. This should be considered legacy.
87 | //
88 | // The constructed editor object has the methods:
89 | // - getConverter() returns the markdown converter object that was passed to the constructor
90 | // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
91 | // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
92 | Markdown.Editor = function (markdownConverter, idPostfix, options) {
93 |
94 | options = options || {};
95 |
96 | if (typeof options.handler === "function") { //backwards compatible behavior
97 | options = { helpButton: options };
98 | }
99 | options.strings = options.strings || {};
100 | if (options.helpButton) {
101 | options.strings.help = options.strings.help || options.helpButton.title;
102 | }
103 | var getString = function (identifier) { return options.strings[identifier] || defaultsStrings[identifier]; }
104 |
105 | idPostfix = idPostfix || "";
106 |
107 | var hooks = this.hooks = new Markdown.HookCollection();
108 | hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed
109 | hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
110 | hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates
111 | * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
112 | * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
113 | */
114 |
115 | this.getConverter = function () { return markdownConverter; }
116 |
117 | var that = this,
118 | panels;
119 |
120 | this.run = function () {
121 | if (panels)
122 | return; // already initialized
123 |
124 | panels = new PanelCollection(idPostfix);
125 | var commandManager = new CommandManager(hooks, getString, markdownConverter);
126 | var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });
127 | var undoManager, uiManager;
128 |
129 | if (!/\?noundo/.test(doc.location.href)) {
130 | undoManager = new UndoManager(function () {
131 | previewManager.refresh();
132 | if (uiManager) // not available on the first call
133 | uiManager.setUndoRedoButtonStates();
134 | }, panels);
135 | this.textOperation = function (f) {
136 | undoManager.setCommandMode();
137 | f();
138 | that.refreshPreview();
139 | }
140 | }
141 |
142 | uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString);
143 | uiManager.setUndoRedoButtonStates();
144 |
145 | var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
146 |
147 | forceRefresh();
148 | };
149 |
150 | }
151 |
152 | // before: contains all the text in the input box BEFORE the selection.
153 | // after: contains all the text in the input box AFTER the selection.
154 | function Chunks() { }
155 |
156 | // startRegex: a regular expression to find the start tag
157 | // endRegex: a regular expresssion to find the end tag
158 | Chunks.prototype.findTags = function (startRegex, endRegex) {
159 |
160 | var chunkObj = this;
161 | var regex;
162 |
163 | if (startRegex) {
164 |
165 | regex = util.extendRegExp(startRegex, "", "$");
166 |
167 | this.before = this.before.replace(regex,
168 | function (match) {
169 | chunkObj.startTag = chunkObj.startTag + match;
170 | return "";
171 | });
172 |
173 | regex = util.extendRegExp(startRegex, "^", "");
174 |
175 | this.selection = this.selection.replace(regex,
176 | function (match) {
177 | chunkObj.startTag = chunkObj.startTag + match;
178 | return "";
179 | });
180 | }
181 |
182 | if (endRegex) {
183 |
184 | regex = util.extendRegExp(endRegex, "", "$");
185 |
186 | this.selection = this.selection.replace(regex,
187 | function (match) {
188 | chunkObj.endTag = match + chunkObj.endTag;
189 | return "";
190 | });
191 |
192 | regex = util.extendRegExp(endRegex, "^", "");
193 |
194 | this.after = this.after.replace(regex,
195 | function (match) {
196 | chunkObj.endTag = match + chunkObj.endTag;
197 | return "";
198 | });
199 | }
200 | };
201 |
202 | // If remove is false, the whitespace is transferred
203 | // to the before/after regions.
204 | //
205 | // If remove is true, the whitespace disappears.
206 | Chunks.prototype.trimWhitespace = function (remove) {
207 | var beforeReplacer, afterReplacer, that = this;
208 | if (remove) {
209 | beforeReplacer = afterReplacer = "";
210 | } else {
211 | beforeReplacer = function (s) { that.before += s; return ""; }
212 | afterReplacer = function (s) { that.after = s + that.after; return ""; }
213 | }
214 |
215 | this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
216 | };
217 |
218 |
219 | Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
220 |
221 | if (nLinesBefore === undefined) {
222 | nLinesBefore = 1;
223 | }
224 |
225 | if (nLinesAfter === undefined) {
226 | nLinesAfter = 1;
227 | }
228 |
229 | nLinesBefore++;
230 | nLinesAfter++;
231 |
232 | var regexText;
233 | var replacementText;
234 |
235 | // chrome bug ... documented at: http://meta.stackexchange.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
236 | if (navigator.userAgent.match(/Chrome/)) {
237 | "X".match(/()./);
238 | }
239 |
240 | this.selection = this.selection.replace(/(^\n*)/, "");
241 |
242 | this.startTag = this.startTag + re.$1;
243 |
244 | this.selection = this.selection.replace(/(\n*$)/, "");
245 | this.endTag = this.endTag + re.$1;
246 | this.startTag = this.startTag.replace(/(^\n*)/, "");
247 | this.before = this.before + re.$1;
248 | this.endTag = this.endTag.replace(/(\n*$)/, "");
249 | this.after = this.after + re.$1;
250 |
251 | if (this.before) {
252 |
253 | regexText = replacementText = "";
254 |
255 | while (nLinesBefore--) {
256 | regexText += "\\n?";
257 | replacementText += "\n";
258 | }
259 |
260 | if (findExtraNewlines) {
261 | regexText = "\\n*";
262 | }
263 | this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
264 | }
265 |
266 | if (this.after) {
267 |
268 | regexText = replacementText = "";
269 |
270 | while (nLinesAfter--) {
271 | regexText += "\\n?";
272 | replacementText += "\n";
273 | }
274 | if (findExtraNewlines) {
275 | regexText = "\\n*";
276 | }
277 |
278 | this.after = this.after.replace(new re(regexText, ""), replacementText);
279 | }
280 | };
281 |
282 | // end of Chunks
283 |
284 | // A collection of the important regions on the page.
285 | // Cached so we don't have to keep traversing the DOM.
286 | // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
287 | // this issue:
288 | // Internet explorer has problems with CSS sprite buttons that use HTML
289 | // lists. When you click on the background image "button", IE will
290 | // select the non-existent link text and discard the selection in the
291 | // textarea. The solution to this is to cache the textarea selection
292 | // on the button's mousedown event and set a flag. In the part of the
293 | // code where we need to grab the selection, we check for the flag
294 | // and, if it's set, use the cached area instead of querying the
295 | // textarea.
296 | //
297 | // This ONLY affects Internet Explorer (tested on versions 6, 7
298 | // and 8) and ONLY on button clicks. Keyboard shortcuts work
299 | // normally since the focus never leaves the textarea.
300 | function PanelCollection(postfix) {
301 | this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
302 | this.preview = doc.getElementById("wmd-preview" + postfix);
303 | this.input = doc.getElementById("wmd-input" + postfix);
304 | };
305 |
306 | // Returns true if the DOM element is visible, false if it's hidden.
307 | // Checks if display is anything other than none.
308 | util.isVisible = function (elem) {
309 |
310 | if (window.getComputedStyle) {
311 | // Most browsers
312 | return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
313 | }
314 | else if (elem.currentStyle) {
315 | // IE
316 | return elem.currentStyle["display"] !== "none";
317 | }
318 | };
319 |
320 |
321 | // Adds a listener callback to a DOM element which is fired on a specified
322 | // event.
323 | util.addEvent = function (elem, event, listener) {
324 | if (elem.attachEvent) {
325 | // IE only. The "on" is mandatory.
326 | elem.attachEvent("on" + event, listener);
327 | }
328 | else {
329 | // Other browsers.
330 | elem.addEventListener(event, listener, false);
331 | }
332 | };
333 |
334 |
335 | // Removes a listener callback from a DOM element which is fired on a specified
336 | // event.
337 | util.removeEvent = function (elem, event, listener) {
338 | if (elem.detachEvent) {
339 | // IE only. The "on" is mandatory.
340 | elem.detachEvent("on" + event, listener);
341 | }
342 | else {
343 | // Other browsers.
344 | elem.removeEventListener(event, listener, false);
345 | }
346 | };
347 |
348 | // Converts \r\n and \r to \n.
349 | util.fixEolChars = function (text) {
350 | text = text.replace(/\r\n/g, "\n");
351 | text = text.replace(/\r/g, "\n");
352 | return text;
353 | };
354 |
355 | // Extends a regular expression. Returns a new RegExp
356 | // using pre + regex + post as the expression.
357 | // Used in a few functions where we have a base
358 | // expression and we want to pre- or append some
359 | // conditions to it (e.g. adding "$" to the end).
360 | // The flags are unchanged.
361 | //
362 | // regex is a RegExp, pre and post are strings.
363 | util.extendRegExp = function (regex, pre, post) {
364 |
365 | if (pre === null || pre === undefined) {
366 | pre = "";
367 | }
368 | if (post === null || post === undefined) {
369 | post = "";
370 | }
371 |
372 | var pattern = regex.toString();
373 | var flags;
374 |
375 | // Replace the flags with empty space and store them.
376 | pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) {
377 | flags = flagsPart;
378 | return "";
379 | });
380 |
381 | // Remove the slash delimiters on the regular expression.
382 | pattern = pattern.replace(/(^\/|\/$)/g, "");
383 | pattern = pre + pattern + post;
384 |
385 | return new re(pattern, flags);
386 | }
387 |
388 | // UNFINISHED
389 | // The assignment in the while loop makes jslint cranky.
390 | // I'll change it to a better loop later.
391 | position.getTop = function (elem, isInner) {
392 | var result = elem.offsetTop;
393 | if (!isInner) {
394 | while (elem = elem.offsetParent) {
395 | result += elem.offsetTop;
396 | }
397 | }
398 | return result;
399 | };
400 |
401 | position.getHeight = function (elem) {
402 | return elem.offsetHeight || elem.scrollHeight;
403 | };
404 |
405 | position.getWidth = function (elem) {
406 | return elem.offsetWidth || elem.scrollWidth;
407 | };
408 |
409 | position.getPageSize = function () {
410 |
411 | var scrollWidth, scrollHeight;
412 | var innerWidth, innerHeight;
413 |
414 | // It's not very clear which blocks work with which browsers.
415 | if (self.innerHeight && self.scrollMaxY) {
416 | scrollWidth = doc.body.scrollWidth;
417 | scrollHeight = self.innerHeight + self.scrollMaxY;
418 | }
419 | else if (doc.body.scrollHeight > doc.body.offsetHeight) {
420 | scrollWidth = doc.body.scrollWidth;
421 | scrollHeight = doc.body.scrollHeight;
422 | }
423 | else {
424 | scrollWidth = doc.body.offsetWidth;
425 | scrollHeight = doc.body.offsetHeight;
426 | }
427 |
428 | if (self.innerHeight) {
429 | // Non-IE browser
430 | innerWidth = self.innerWidth;
431 | innerHeight = self.innerHeight;
432 | }
433 | else if (doc.documentElement && doc.documentElement.clientHeight) {
434 | // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
435 | innerWidth = doc.documentElement.clientWidth;
436 | innerHeight = doc.documentElement.clientHeight;
437 | }
438 | else if (doc.body) {
439 | // Other versions of IE
440 | innerWidth = doc.body.clientWidth;
441 | innerHeight = doc.body.clientHeight;
442 | }
443 |
444 | var maxWidth = Math.max(scrollWidth, innerWidth);
445 | var maxHeight = Math.max(scrollHeight, innerHeight);
446 | return [maxWidth, maxHeight, innerWidth, innerHeight];
447 | };
448 |
449 | // Handles pushing and popping TextareaStates for undo/redo commands.
450 | // I should rename the stack variables to list.
451 | function UndoManager(callback, panels) {
452 |
453 | var undoObj = this;
454 | var undoStack = []; // A stack of undo states
455 | var stackPtr = 0; // The index of the current state
456 | var mode = "none";
457 | var lastState; // The last state
458 | var timer; // The setTimeout handle for cancelling the timer
459 | var inputStateObj;
460 |
461 | // Set the mode for later logic steps.
462 | var setMode = function (newMode, noSave) {
463 | if (mode != newMode) {
464 | mode = newMode;
465 | if (!noSave) {
466 | saveState();
467 | }
468 | }
469 |
470 | if (!uaSniffed.isIE || mode != "moving") {
471 | timer = setTimeout(refreshState, 1);
472 | }
473 | else {
474 | inputStateObj = null;
475 | }
476 | };
477 |
478 | var refreshState = function (isInitialState) {
479 | inputStateObj = new TextareaState(panels, isInitialState);
480 | timer = undefined;
481 | };
482 |
483 | this.setCommandMode = function () {
484 | mode = "command";
485 | saveState();
486 | timer = setTimeout(refreshState, 0);
487 | };
488 |
489 | this.canUndo = function () {
490 | return stackPtr > 1;
491 | };
492 |
493 | this.canRedo = function () {
494 | if (undoStack[stackPtr + 1]) {
495 | return true;
496 | }
497 | return false;
498 | };
499 |
500 | // Removes the last state and restores it.
501 | this.undo = function () {
502 |
503 | if (undoObj.canUndo()) {
504 | if (lastState) {
505 | // What about setting state -1 to null or checking for undefined?
506 | lastState.restore();
507 | lastState = null;
508 | }
509 | else {
510 | undoStack[stackPtr] = new TextareaState(panels);
511 | undoStack[--stackPtr].restore();
512 |
513 | if (callback) {
514 | callback();
515 | }
516 | }
517 | }
518 |
519 | mode = "none";
520 | panels.input.focus();
521 | refreshState();
522 | };
523 |
524 | // Redo an action.
525 | this.redo = function () {
526 |
527 | if (undoObj.canRedo()) {
528 |
529 | undoStack[++stackPtr].restore();
530 |
531 | if (callback) {
532 | callback();
533 | }
534 | }
535 |
536 | mode = "none";
537 | panels.input.focus();
538 | refreshState();
539 | };
540 |
541 | // Push the input area state to the stack.
542 | var saveState = function () {
543 | var currState = inputStateObj || new TextareaState(panels);
544 |
545 | if (!currState) {
546 | return false;
547 | }
548 | if (mode == "moving") {
549 | if (!lastState) {
550 | lastState = currState;
551 | }
552 | return;
553 | }
554 | if (lastState) {
555 | if (undoStack[stackPtr - 1].text != lastState.text) {
556 | undoStack[stackPtr++] = lastState;
557 | }
558 | lastState = null;
559 | }
560 | undoStack[stackPtr++] = currState;
561 | undoStack[stackPtr + 1] = null;
562 | if (callback) {
563 | callback();
564 | }
565 | };
566 |
567 | var handleCtrlYZ = function (event) {
568 |
569 | var handled = false;
570 |
571 | if ((event.ctrlKey || event.metaKey) && !event.altKey) {
572 |
573 | // IE and Opera do not support charCode.
574 | var keyCode = event.charCode || event.keyCode;
575 | var keyCodeChar = String.fromCharCode(keyCode);
576 |
577 | switch (keyCodeChar.toLowerCase()) {
578 |
579 | case "y":
580 | undoObj.redo();
581 | handled = true;
582 | break;
583 |
584 | case "z":
585 | if (!event.shiftKey) {
586 | undoObj.undo();
587 | }
588 | else {
589 | undoObj.redo();
590 | }
591 | handled = true;
592 | break;
593 | }
594 | }
595 |
596 | if (handled) {
597 | if (event.preventDefault) {
598 | event.preventDefault();
599 | }
600 | if (window.event) {
601 | window.event.returnValue = false;
602 | }
603 | return;
604 | }
605 | };
606 |
607 | // Set the mode depending on what is going on in the input area.
608 | var handleModeChange = function (event) {
609 |
610 | if (!event.ctrlKey && !event.metaKey) {
611 |
612 | var keyCode = event.keyCode;
613 |
614 | if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
615 | // 33 - 40: page up/dn and arrow keys
616 | // 63232 - 63235: page up/dn and arrow keys on safari
617 | setMode("moving");
618 | }
619 | else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
620 | // 8: backspace
621 | // 46: delete
622 | // 127: delete
623 | setMode("deleting");
624 | }
625 | else if (keyCode == 13) {
626 | // 13: Enter
627 | setMode("newlines");
628 | }
629 | else if (keyCode == 27) {
630 | // 27: escape
631 | setMode("escape");
632 | }
633 | else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
634 | // 16-20 are shift, etc.
635 | // 91: left window key
636 | // I think this might be a little messed up since there are
637 | // a lot of nonprinting keys above 20.
638 | setMode("typing");
639 | }
640 | }
641 | };
642 |
643 | var setEventHandlers = function () {
644 | util.addEvent(panels.input, "keypress", function (event) {
645 | // keyCode 89: y
646 | // keyCode 90: z
647 | if ((event.ctrlKey || event.metaKey) && !event.altKey && (event.keyCode == 89 || event.keyCode == 90)) {
648 | event.preventDefault();
649 | }
650 | });
651 |
652 | var handlePaste = function () {
653 | if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {
654 | if (timer == undefined) {
655 | mode = "paste";
656 | saveState();
657 | refreshState();
658 | }
659 | }
660 | };
661 |
662 | util.addEvent(panels.input, "keydown", handleCtrlYZ);
663 | util.addEvent(panels.input, "keydown", handleModeChange);
664 | util.addEvent(panels.input, "mousedown", function () {
665 | setMode("moving");
666 | });
667 |
668 | panels.input.onpaste = handlePaste;
669 | panels.input.ondrop = handlePaste;
670 | };
671 |
672 | var init = function () {
673 | setEventHandlers();
674 | refreshState(true);
675 | saveState();
676 | };
677 |
678 | init();
679 | }
680 |
681 | // end of UndoManager
682 |
683 | // The input textarea state/contents.
684 | // This is used to implement undo/redo by the undo manager.
685 | function TextareaState(panels, isInitialState) {
686 |
687 | // Aliases
688 | var stateObj = this;
689 | var inputArea = panels.input;
690 | this.init = function () {
691 | if (!util.isVisible(inputArea)) {
692 | return;
693 | }
694 | if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box
695 | return;
696 | }
697 |
698 | this.setInputAreaSelectionStartEnd();
699 | this.scrollTop = inputArea.scrollTop;
700 | if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
701 | this.text = inputArea.value;
702 | }
703 |
704 | }
705 |
706 | // Sets the selected text in the input box after we've performed an
707 | // operation.
708 | this.setInputAreaSelection = function () {
709 |
710 | if (!util.isVisible(inputArea)) {
711 | return;
712 | }
713 |
714 | if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {
715 |
716 | inputArea.focus();
717 | inputArea.selectionStart = stateObj.start;
718 | inputArea.selectionEnd = stateObj.end;
719 | inputArea.scrollTop = stateObj.scrollTop;
720 | }
721 | else if (doc.selection) {
722 |
723 | if (doc.activeElement && doc.activeElement !== inputArea) {
724 | return;
725 | }
726 |
727 | inputArea.focus();
728 | var range = inputArea.createTextRange();
729 | range.moveStart("character", -inputArea.value.length);
730 | range.moveEnd("character", -inputArea.value.length);
731 | range.moveEnd("character", stateObj.end);
732 | range.moveStart("character", stateObj.start);
733 | range.select();
734 | }
735 | };
736 |
737 | this.setInputAreaSelectionStartEnd = function () {
738 |
739 | if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) {
740 |
741 | stateObj.start = inputArea.selectionStart;
742 | stateObj.end = inputArea.selectionEnd;
743 | }
744 | else if (doc.selection) {
745 |
746 | stateObj.text = util.fixEolChars(inputArea.value);
747 |
748 | // IE loses the selection in the textarea when buttons are
749 | // clicked. On IE we cache the selection. Here, if something is cached,
750 | // we take it.
751 | var range = panels.ieCachedRange || doc.selection.createRange();
752 |
753 | var fixedRange = util.fixEolChars(range.text);
754 | var marker = "\x07";
755 | var markedRange = marker + fixedRange + marker;
756 | range.text = markedRange;
757 | var inputText = util.fixEolChars(inputArea.value);
758 |
759 | range.moveStart("character", -markedRange.length);
760 | range.text = fixedRange;
761 |
762 | stateObj.start = inputText.indexOf(marker);
763 | stateObj.end = inputText.lastIndexOf(marker) - marker.length;
764 |
765 | var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
766 |
767 | if (len) {
768 | range.moveStart("character", -fixedRange.length);
769 | while (len--) {
770 | fixedRange += "\n";
771 | stateObj.end += 1;
772 | }
773 | range.text = fixedRange;
774 | }
775 |
776 | if (panels.ieCachedRange)
777 | stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange
778 |
779 | panels.ieCachedRange = null;
780 |
781 | this.setInputAreaSelection();
782 | }
783 | };
784 |
785 | // Restore this state into the input area.
786 | this.restore = function () {
787 |
788 | if (stateObj.text != undefined && stateObj.text != inputArea.value) {
789 | inputArea.value = stateObj.text;
790 | }
791 | this.setInputAreaSelection();
792 | inputArea.scrollTop = stateObj.scrollTop;
793 | };
794 |
795 | // Gets a collection of HTML chunks from the inptut textarea.
796 | this.getChunks = function () {
797 |
798 | var chunk = new Chunks();
799 | chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
800 | chunk.startTag = "";
801 | chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
802 | chunk.endTag = "";
803 | chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
804 | chunk.scrollTop = stateObj.scrollTop;
805 |
806 | return chunk;
807 | };
808 |
809 | // Sets the TextareaState properties given a chunk of markdown.
810 | this.setChunks = function (chunk) {
811 |
812 | chunk.before = chunk.before + chunk.startTag;
813 | chunk.after = chunk.endTag + chunk.after;
814 |
815 | this.start = chunk.before.length;
816 | this.end = chunk.before.length + chunk.selection.length;
817 | this.text = chunk.before + chunk.selection + chunk.after;
818 | this.scrollTop = chunk.scrollTop;
819 | };
820 | this.init();
821 | };
822 |
823 | function PreviewManager(converter, panels, previewRefreshCallback) {
824 |
825 | var managerObj = this;
826 | var timeout;
827 | var elapsedTime;
828 | var oldInputText;
829 | var maxDelay = 3000;
830 | var startType = "delayed"; // The other legal value is "manual"
831 |
832 | // Adds event listeners to elements
833 | var setupEvents = function (inputElem, listener) {
834 |
835 | util.addEvent(inputElem, "input", listener);
836 | inputElem.onpaste = listener;
837 | inputElem.ondrop = listener;
838 |
839 | util.addEvent(inputElem, "keypress", listener);
840 | util.addEvent(inputElem, "keydown", listener);
841 | };
842 |
843 | var getDocScrollTop = function () {
844 |
845 | var result = 0;
846 |
847 | if (window.innerHeight) {
848 | result = window.pageYOffset;
849 | }
850 | else
851 | if (doc.documentElement && doc.documentElement.scrollTop) {
852 | result = doc.documentElement.scrollTop;
853 | }
854 | else
855 | if (doc.body) {
856 | result = doc.body.scrollTop;
857 | }
858 |
859 | return result;
860 | };
861 |
862 | var makePreviewHtml = function () {
863 |
864 | // If there is no registered preview panel
865 | // there is nothing to do.
866 | if (!panels.preview)
867 | return;
868 |
869 |
870 | var text = panels.input.value;
871 | if (text && text == oldInputText) {
872 | return; // Input text hasn't changed.
873 | }
874 | else {
875 | oldInputText = text;
876 | }
877 |
878 | var prevTime = new Date().getTime();
879 |
880 | text = converter.makeHtml(text);
881 |
882 | // Calculate the processing time of the HTML creation.
883 | // It's used as the delay time in the event listener.
884 | var currTime = new Date().getTime();
885 | elapsedTime = currTime - prevTime;
886 |
887 | pushPreviewHtml(text);
888 | };
889 |
890 | // setTimeout is already used. Used as an event listener.
891 | var applyTimeout = function () {
892 |
893 | if (timeout) {
894 | clearTimeout(timeout);
895 | timeout = undefined;
896 | }
897 |
898 | if (startType !== "manual") {
899 |
900 | var delay = 0;
901 |
902 | if (startType === "delayed") {
903 | delay = elapsedTime;
904 | }
905 |
906 | if (delay > maxDelay) {
907 | delay = maxDelay;
908 | }
909 | timeout = setTimeout(makePreviewHtml, delay);
910 | }
911 | };
912 |
913 | var getScaleFactor = function (panel) {
914 | if (panel.scrollHeight <= panel.clientHeight) {
915 | return 1;
916 | }
917 | return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
918 | };
919 |
920 | var setPanelScrollTops = function () {
921 | if (panels.preview) {
922 | panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
923 | }
924 | };
925 |
926 | this.refresh = function (requiresRefresh) {
927 |
928 | if (requiresRefresh) {
929 | oldInputText = "";
930 | makePreviewHtml();
931 | }
932 | else {
933 | applyTimeout();
934 | }
935 | };
936 |
937 | this.processingTime = function () {
938 | return elapsedTime;
939 | };
940 |
941 | var isFirstTimeFilled = true;
942 |
943 | // IE doesn't let you use innerHTML if the element is contained somewhere in a table
944 | // (which is the case for inline editing) -- in that case, detach the element, set the
945 | // value, and reattach. Yes, that *is* ridiculous.
946 | var ieSafePreviewSet = function (text) {
947 | var preview = panels.preview;
948 | var parent = preview.parentNode;
949 | var sibling = preview.nextSibling;
950 | parent.removeChild(preview);
951 | preview.innerHTML = text;
952 | if (!sibling)
953 | parent.appendChild(preview);
954 | else
955 | parent.insertBefore(preview, sibling);
956 | }
957 |
958 | var nonSuckyBrowserPreviewSet = function (text) {
959 | panels.preview.innerHTML = text;
960 | }
961 |
962 | var previewSetter;
963 |
964 | var previewSet = function (text) {
965 | if (previewSetter)
966 | return previewSetter(text);
967 |
968 | try {
969 | nonSuckyBrowserPreviewSet(text);
970 | previewSetter = nonSuckyBrowserPreviewSet;
971 | } catch (e) {
972 | previewSetter = ieSafePreviewSet;
973 | previewSetter(text);
974 | }
975 | };
976 |
977 | var pushPreviewHtml = function (text) {
978 |
979 | var emptyTop = position.getTop(panels.input) - getDocScrollTop();
980 |
981 | if (panels.preview) {
982 | previewSet(text);
983 | previewRefreshCallback();
984 | }
985 |
986 | setPanelScrollTops();
987 |
988 | if (isFirstTimeFilled) {
989 | isFirstTimeFilled = false;
990 | return;
991 | }
992 |
993 | var fullTop = position.getTop(panels.input) - getDocScrollTop();
994 |
995 | if (uaSniffed.isIE) {
996 | setTimeout(function () {
997 | window.scrollBy(0, fullTop - emptyTop);
998 | }, 0);
999 | }
1000 | else {
1001 | window.scrollBy(0, fullTop - emptyTop);
1002 | }
1003 | };
1004 |
1005 | var init = function () {
1006 |
1007 | setupEvents(panels.input, applyTimeout);
1008 | makePreviewHtml();
1009 |
1010 | if (panels.preview) {
1011 | panels.preview.scrollTop = 0;
1012 | }
1013 | };
1014 |
1015 | init();
1016 | };
1017 |
1018 | // Creates the background behind the hyperlink text entry box.
1019 | // And download dialog
1020 | // Most of this has been moved to CSS but the div creation and
1021 | // browser-specific hacks remain here.
1022 | ui.createBackground = function () {
1023 |
1024 | var background = doc.createElement("div"),
1025 | style = background.style;
1026 |
1027 | background.className = "wmd-prompt-background";
1028 |
1029 | style.position = "absolute";
1030 | style.top = "0";
1031 |
1032 | style.zIndex = "1000";
1033 |
1034 | if (uaSniffed.isIE) {
1035 | style.filter = "alpha(opacity=50)";
1036 | }
1037 | else {
1038 | style.opacity = "0.5";
1039 | }
1040 |
1041 | var pageSize = position.getPageSize();
1042 | style.height = pageSize[1] + "px";
1043 |
1044 | if (uaSniffed.isIE) {
1045 | style.left = doc.documentElement.scrollLeft;
1046 | style.width = doc.documentElement.clientWidth;
1047 | }
1048 | else {
1049 | style.left = "0";
1050 | style.width = "100%";
1051 | }
1052 |
1053 | doc.body.appendChild(background);
1054 | return background;
1055 | };
1056 |
1057 | // This simulates a modal dialog box and asks for the URL when you
1058 | // click the hyperlink or image buttons.
1059 | //
1060 | // text: The html for the input box.
1061 | // defaultInputText: The default value that appears in the input box.
1062 | // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
1063 | // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
1064 | // was chosen).
1065 | ui.prompt = function (text, defaultInputText, callback) {
1066 |
1067 | // These variables need to be declared at this level since they are used
1068 | // in multiple functions.
1069 | var dialog; // The dialog box.
1070 | var input; // The text box where you enter the hyperlink.
1071 |
1072 |
1073 | if (defaultInputText === undefined) {
1074 | defaultInputText = "";
1075 | }
1076 |
1077 | // Used as a keydown event handler. Esc dismisses the prompt.
1078 | // Key code 27 is ESC.
1079 | var checkEscape = function (key) {
1080 | var code = (key.charCode || key.keyCode);
1081 | if (code === 27) {
1082 | if (key.stopPropagation) key.stopPropagation();
1083 | close(true);
1084 | return false;
1085 | }
1086 | };
1087 |
1088 | // Dismisses the hyperlink input box.
1089 | // isCancel is true if we don't care about the input text.
1090 | // isCancel is false if we are going to keep the text.
1091 | var close = function (isCancel) {
1092 | util.removeEvent(doc.body, "keyup", checkEscape);
1093 | var text = input.value;
1094 |
1095 | if (isCancel) {
1096 | text = null;
1097 | }
1098 | else {
1099 | // Fixes common pasting errors.
1100 | text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
1101 | if (!/^(?:https?|ftp):\/\//.test(text))
1102 | text = 'http://' + text;
1103 | }
1104 |
1105 | dialog.parentNode.removeChild(dialog);
1106 |
1107 | callback(text);
1108 | return false;
1109 | };
1110 |
1111 |
1112 |
1113 | // Create the text input box form/window.
1114 | var createDialog = function () {
1115 |
1116 | // The main dialog box.
1117 | dialog = doc.createElement("div");
1118 | dialog.className = "wmd-prompt-dialog";
1119 | dialog.style.padding = "10px;";
1120 | dialog.style.position = "fixed";
1121 | dialog.style.width = "400px";
1122 | dialog.style.zIndex = "1001";
1123 |
1124 | // The dialog text.
1125 | var question = doc.createElement("div");
1126 | question.innerHTML = text;
1127 | question.style.padding = "5px";
1128 | dialog.appendChild(question);
1129 |
1130 | // The web form container for the text box and buttons.
1131 | var form = doc.createElement("form"),
1132 | style = form.style;
1133 | form.onsubmit = function () { return close(false); };
1134 | style.padding = "0";
1135 | style.margin = "0";
1136 | style.cssFloat = "left";
1137 | style.width = "100%";
1138 | style.textAlign = "center";
1139 | style.position = "relative";
1140 | dialog.appendChild(form);
1141 |
1142 | // The input text box
1143 | input = doc.createElement("input");
1144 | input.type = "text";
1145 | input.value = defaultInputText;
1146 | style = input.style;
1147 | style.display = "block";
1148 | style.width = "80%";
1149 | style.marginLeft = style.marginRight = "auto";
1150 | form.appendChild(input);
1151 |
1152 | // The ok button
1153 | var okButton = doc.createElement("input");
1154 | okButton.type = "button";
1155 | okButton.onclick = function () { return close(false); };
1156 | okButton.value = "OK";
1157 | style = okButton.style;
1158 | style.margin = "10px";
1159 | style.display = "inline";
1160 | style.width = "7em";
1161 |
1162 |
1163 | // The cancel button
1164 | var cancelButton = doc.createElement("input");
1165 | cancelButton.type = "button";
1166 | cancelButton.onclick = function () { return close(true); };
1167 | cancelButton.value = "Cancel";
1168 | style = cancelButton.style;
1169 | style.margin = "10px";
1170 | style.display = "inline";
1171 | style.width = "7em";
1172 |
1173 | form.appendChild(okButton);
1174 | form.appendChild(cancelButton);
1175 |
1176 | util.addEvent(doc.body, "keyup", checkEscape);
1177 | dialog.style.top = "50%";
1178 | dialog.style.left = "50%";
1179 | dialog.style.display = "block";
1180 | if (uaSniffed.isIE_5or6) {
1181 | dialog.style.position = "absolute";
1182 | dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
1183 | dialog.style.left = "50%";
1184 | }
1185 | doc.body.appendChild(dialog);
1186 |
1187 | // This has to be done AFTER adding the dialog to the form if you
1188 | // want it to be centered.
1189 | dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
1190 | dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
1191 |
1192 | };
1193 |
1194 | // Why is this in a zero-length timeout?
1195 | // Is it working around a browser bug?
1196 | setTimeout(function () {
1197 |
1198 | createDialog();
1199 |
1200 | var defTextLen = defaultInputText.length;
1201 | if (input.selectionStart !== undefined) {
1202 | input.selectionStart = 0;
1203 | input.selectionEnd = defTextLen;
1204 | }
1205 | else if (input.createTextRange) {
1206 | var range = input.createTextRange();
1207 | range.collapse(false);
1208 | range.moveStart("character", -defTextLen);
1209 | range.moveEnd("character", defTextLen);
1210 | range.select();
1211 | }
1212 |
1213 | input.focus();
1214 | }, 0);
1215 | };
1216 |
1217 | function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) {
1218 |
1219 | var inputBox = panels.input,
1220 | buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
1221 |
1222 | makeSpritedButtonRow();
1223 |
1224 | var keyEvent = "keydown";
1225 | if (uaSniffed.isOpera) {
1226 | keyEvent = "keypress";
1227 | }
1228 |
1229 | util.addEvent(inputBox, keyEvent, function (key) {
1230 |
1231 | // Check to see if we have a button key and, if so execute the callback.
1232 | if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
1233 |
1234 | var keyCode = key.charCode || key.keyCode;
1235 | var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
1236 |
1237 | switch (keyCodeStr) {
1238 | case "b":
1239 | doClick(buttons.bold);
1240 | break;
1241 | case "i":
1242 | doClick(buttons.italic);
1243 | break;
1244 | case "l":
1245 | doClick(buttons.link);
1246 | break;
1247 | case "q":
1248 | doClick(buttons.quote);
1249 | break;
1250 | case "k":
1251 | doClick(buttons.code);
1252 | break;
1253 | case "g":
1254 | doClick(buttons.image);
1255 | break;
1256 | case "o":
1257 | doClick(buttons.olist);
1258 | break;
1259 | case "u":
1260 | doClick(buttons.ulist);
1261 | break;
1262 | case "h":
1263 | doClick(buttons.heading);
1264 | break;
1265 | case "r":
1266 | doClick(buttons.hr);
1267 | break;
1268 | case "y":
1269 | doClick(buttons.redo);
1270 | break;
1271 | case "z":
1272 | if (key.shiftKey) {
1273 | doClick(buttons.redo);
1274 | }
1275 | else {
1276 | doClick(buttons.undo);
1277 | }
1278 | break;
1279 | default:
1280 | return;
1281 | }
1282 |
1283 |
1284 | if (key.preventDefault) {
1285 | key.preventDefault();
1286 | }
1287 |
1288 | if (window.event) {
1289 | window.event.returnValue = false;
1290 | }
1291 | }
1292 | });
1293 |
1294 | // Auto-indent on shift-enter
1295 | util.addEvent(inputBox, "keyup", function (key) {
1296 | if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
1297 | var keyCode = key.charCode || key.keyCode;
1298 | // Character 13 is Enter
1299 | if (keyCode === 13) {
1300 | var fakeButton = {};
1301 | fakeButton.textOp = bindCommand("doAutoindent");
1302 | doClick(fakeButton);
1303 | }
1304 | }
1305 | });
1306 |
1307 | // special handler because IE clears the context of the textbox on ESC
1308 | if (uaSniffed.isIE) {
1309 | util.addEvent(inputBox, "keydown", function (key) {
1310 | var code = key.keyCode;
1311 | if (code === 27) {
1312 | return false;
1313 | }
1314 | });
1315 | }
1316 |
1317 |
1318 | // Perform the button's action.
1319 | function doClick(button) {
1320 |
1321 | inputBox.focus();
1322 |
1323 | if (button.textOp) {
1324 |
1325 | if (undoManager) {
1326 | undoManager.setCommandMode();
1327 | }
1328 |
1329 | var state = new TextareaState(panels);
1330 |
1331 | if (!state) {
1332 | return;
1333 | }
1334 |
1335 | var chunks = state.getChunks();
1336 |
1337 | // Some commands launch a "modal" prompt dialog. Javascript
1338 | // can't really make a modal dialog box and the WMD code
1339 | // will continue to execute while the dialog is displayed.
1340 | // This prevents the dialog pattern I'm used to and means
1341 | // I can't do something like this:
1342 | //
1343 | // var link = CreateLinkDialog();
1344 | // makeMarkdownLink(link);
1345 | //
1346 | // Instead of this straightforward method of handling a
1347 | // dialog I have to pass any code which would execute
1348 | // after the dialog is dismissed (e.g. link creation)
1349 | // in a function parameter.
1350 | //
1351 | // Yes this is awkward and I think it sucks, but there's
1352 | // no real workaround. Only the image and link code
1353 | // create dialogs and require the function pointers.
1354 | var fixupInputArea = function () {
1355 |
1356 | inputBox.focus();
1357 |
1358 | if (chunks) {
1359 | state.setChunks(chunks);
1360 | }
1361 |
1362 | state.restore();
1363 | previewManager.refresh();
1364 | };
1365 |
1366 | var noCleanup = button.textOp(chunks, fixupInputArea);
1367 |
1368 | if (!noCleanup) {
1369 | fixupInputArea();
1370 | }
1371 |
1372 | }
1373 |
1374 | if (button.execute) {
1375 | button.execute(undoManager);
1376 | }
1377 | };
1378 |
1379 | function setupButton(button, isEnabled) {
1380 |
1381 | var normalYShift = "0px";
1382 | var disabledYShift = "-20px";
1383 | var highlightYShift = "-40px";
1384 | var image = button.getElementsByTagName("span")[0];
1385 | if (isEnabled) {
1386 | image.style.backgroundPosition = button.XShift + " " + normalYShift;
1387 | button.onmouseover = function () {
1388 | image.style.backgroundPosition = this.XShift + " " + highlightYShift;
1389 | };
1390 |
1391 | button.onmouseout = function () {
1392 | image.style.backgroundPosition = this.XShift + " " + normalYShift;
1393 | };
1394 |
1395 | // IE tries to select the background image "button" text (it's
1396 | // implemented in a list item) so we have to cache the selection
1397 | // on mousedown.
1398 | if (uaSniffed.isIE) {
1399 | button.onmousedown = function () {
1400 | if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection
1401 | return;
1402 | }
1403 | panels.ieCachedRange = document.selection.createRange();
1404 | panels.ieCachedScrollTop = panels.input.scrollTop;
1405 | };
1406 | }
1407 |
1408 | if (!button.isHelp) {
1409 | button.onclick = function () {
1410 | if (this.onmouseout) {
1411 | this.onmouseout();
1412 | }
1413 | doClick(this);
1414 | return false;
1415 | }
1416 | }
1417 | }
1418 | else {
1419 | image.style.backgroundPosition = button.XShift + " " + disabledYShift;
1420 | button.onmouseover = button.onmouseout = button.onclick = function () { };
1421 | }
1422 | }
1423 |
1424 | function bindCommand(method) {
1425 | if (typeof method === "string")
1426 | method = commandManager[method];
1427 | return function () { method.apply(commandManager, arguments); }
1428 | }
1429 |
1430 | function makeSpritedButtonRow() {
1431 |
1432 | var buttonBar = panels.buttonBar;
1433 |
1434 | var normalYShift = "0px";
1435 | var disabledYShift = "-20px";
1436 | var highlightYShift = "-40px";
1437 |
1438 | var buttonRow = document.createElement("ul");
1439 | buttonRow.id = "wmd-button-row" + postfix;
1440 | buttonRow.className = 'wmd-button-row';
1441 | buttonRow = buttonBar.appendChild(buttonRow);
1442 | var xPosition = 0;
1443 | var makeButton = function (id, title, XShift, textOp) {
1444 | var button = document.createElement("li");
1445 | button.className = "wmd-button";
1446 | button.style.left = xPosition + "px";
1447 | xPosition += 25;
1448 | var buttonImage = document.createElement("span");
1449 | button.id = id + postfix;
1450 | button.appendChild(buttonImage);
1451 | button.title = title;
1452 | button.XShift = XShift;
1453 | if (textOp)
1454 | button.textOp = textOp;
1455 | setupButton(button, true);
1456 | buttonRow.appendChild(button);
1457 | return button;
1458 | };
1459 | var makeSpacer = function (num) {
1460 | var spacer = document.createElement("li");
1461 | spacer.className = "wmd-spacer wmd-spacer" + num;
1462 | spacer.id = "wmd-spacer" + num + postfix;
1463 | buttonRow.appendChild(spacer);
1464 | xPosition += 25;
1465 | }
1466 |
1467 | buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold"));
1468 | buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic"));
1469 | makeSpacer(1);
1470 | buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function (chunk, postProcessing) {
1471 | return this.doLinkOrImage(chunk, postProcessing, false);
1472 | }));
1473 | buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote"));
1474 | buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode"));
1475 | buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function (chunk, postProcessing) {
1476 | return this.doLinkOrImage(chunk, postProcessing, true);
1477 | }));
1478 | makeSpacer(2);
1479 | buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function (chunk, postProcessing) {
1480 | this.doList(chunk, postProcessing, true);
1481 | }));
1482 | buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function (chunk, postProcessing) {
1483 | this.doList(chunk, postProcessing, false);
1484 | }));
1485 | buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading"));
1486 | buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule"));
1487 | makeSpacer(3);
1488 | buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null);
1489 | buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
1490 |
1491 | var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
1492 | getString("redo") :
1493 | getString("redomac"); // mac and other non-Windows platforms
1494 |
1495 | buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);
1496 | buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
1497 |
1498 | if (helpOptions) {
1499 | var helpButton = document.createElement("li");
1500 | var helpButtonImage = document.createElement("span");
1501 | helpButton.appendChild(helpButtonImage);
1502 | helpButton.className = "wmd-button wmd-help-button";
1503 | helpButton.id = "wmd-help-button" + postfix;
1504 | helpButton.XShift = "-240px";
1505 | helpButton.isHelp = true;
1506 | helpButton.style.right = "0px";
1507 | helpButton.title = getString("help");
1508 | helpButton.onclick = helpOptions.handler;
1509 |
1510 | setupButton(helpButton, true);
1511 | buttonRow.appendChild(helpButton);
1512 | buttons.help = helpButton;
1513 | }
1514 |
1515 | setUndoRedoButtonStates();
1516 | }
1517 |
1518 | function setUndoRedoButtonStates() {
1519 | if (undoManager) {
1520 | setupButton(buttons.undo, undoManager.canUndo());
1521 | setupButton(buttons.redo, undoManager.canRedo());
1522 | }
1523 | };
1524 |
1525 | this.setUndoRedoButtonStates = setUndoRedoButtonStates;
1526 |
1527 | }
1528 |
1529 | function CommandManager(pluginHooks, getString, converter) {
1530 | this.hooks = pluginHooks;
1531 | this.getString = getString;
1532 | this.converter = converter;
1533 | }
1534 |
1535 | var commandProto = CommandManager.prototype;
1536 |
1537 | // The markdown symbols - 4 spaces = code, > = blockquote, etc.
1538 | commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
1539 |
1540 | // Remove markdown symbols from the chunk selection.
1541 | commandProto.unwrap = function (chunk) {
1542 | var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
1543 | chunk.selection = chunk.selection.replace(txt, "$1 $2");
1544 | };
1545 |
1546 | commandProto.wrap = function (chunk, len) {
1547 | this.unwrap(chunk);
1548 | var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"),
1549 | that = this;
1550 |
1551 | chunk.selection = chunk.selection.replace(regex, function (line, marked) {
1552 | if (new re("^" + that.prefixes, "").test(line)) {
1553 | return line;
1554 | }
1555 | return marked + "\n";
1556 | });
1557 |
1558 | chunk.selection = chunk.selection.replace(/\s+$/, "");
1559 | };
1560 |
1561 | commandProto.doBold = function (chunk, postProcessing) {
1562 | return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample"));
1563 | };
1564 |
1565 | commandProto.doItalic = function (chunk, postProcessing) {
1566 | return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample"));
1567 | };
1568 |
1569 | // chunk: The selected region that will be enclosed with */**
1570 | // nStars: 1 for italics, 2 for bold
1571 | // insertText: If you just click the button without highlighting text, this gets inserted
1572 | commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
1573 |
1574 | // Get rid of whitespace and fixup newlines.
1575 | chunk.trimWhitespace();
1576 | chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
1577 |
1578 | // Look for stars before and after. Is the chunk already marked up?
1579 | // note that these regex matches cannot fail
1580 | var starsBefore = /(\**$)/.exec(chunk.before)[0];
1581 | var starsAfter = /(^\**)/.exec(chunk.after)[0];
1582 |
1583 | var prevStars = Math.min(starsBefore.length, starsAfter.length);
1584 |
1585 | // Remove stars if we have to since the button acts as a toggle.
1586 | if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
1587 | chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
1588 | chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
1589 | }
1590 | else if (!chunk.selection && starsAfter) {
1591 | // It's not really clear why this code is necessary. It just moves
1592 | // some arbitrary stuff around.
1593 | chunk.after = chunk.after.replace(/^([*_]*)/, "");
1594 | chunk.before = chunk.before.replace(/(\s?)$/, "");
1595 | var whitespace = re.$1;
1596 | chunk.before = chunk.before + starsAfter + whitespace;
1597 | }
1598 | else {
1599 |
1600 | // In most cases, if you don't have any selected text and click the button
1601 | // you'll get a selected, marked up region with the default text inserted.
1602 | if (!chunk.selection && !starsAfter) {
1603 | chunk.selection = insertText;
1604 | }
1605 |
1606 | // Add the true markup.
1607 | var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
1608 | chunk.before = chunk.before + markup;
1609 | chunk.after = markup + chunk.after;
1610 | }
1611 |
1612 | return;
1613 | };
1614 |
1615 | commandProto.stripLinkDefs = function (text, defsToAdd) {
1616 |
1617 | text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
1618 | function (totalMatch, id, link, newlines, title) {
1619 | defsToAdd[id] = totalMatch.replace(/\s*$/, "");
1620 | if (newlines) {
1621 | // Strip the title and return that separately.
1622 | defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
1623 | return newlines + title;
1624 | }
1625 | return "";
1626 | });
1627 |
1628 | return text;
1629 | };
1630 |
1631 | commandProto.addLinkDef = function (chunk, linkDef) {
1632 |
1633 | var refNumber = 0; // The current reference number
1634 | var defsToAdd = {}; //
1635 | // Start with a clean slate by removing all previous link definitions.
1636 | chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
1637 | chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
1638 | chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
1639 |
1640 | var defs = "";
1641 | var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
1642 |
1643 | // The above regex, used to update [foo][13] references after renumbering,
1644 | // is much too liberal; it can catch things that are not actually parsed
1645 | // as references (notably: code). It's impossible to know which matches are
1646 | // real references without performing a markdown conversion, so that's what
1647 | // we do. All matches are replaced with a unique reference number, which is
1648 | // given a unique link. The uniquifier in both cases is the character offset
1649 | // of the match inside the source string. The modified version is then sent
1650 | // through the Markdown renderer. Because link reference are stripped during
1651 | // rendering, the unique link is present in the rendered version if and only
1652 | // if the match at its offset was in fact rendered as a link or image.
1653 | var complete = chunk.before + chunk.selection + chunk.after;
1654 | var rendered = this.converter.makeHtml(complete);
1655 | var testlink = "http://this-is-a-real-link.biz/";
1656 |
1657 | // If our fake link appears in the rendered version *before* we have added it,
1658 | // this probably means you're a Meta Stack Exchange user who is deliberately
1659 | // trying to break this feature. You can still break this workaround if you
1660 | // attach a plugin to the converter that sometimes (!) inserts this link. In
1661 | // that case, consider yourself unsupported.
1662 | while (rendered.indexOf(testlink) != -1)
1663 | testlink += "nicetry/";
1664 |
1665 | var fakedefs = "\n\n";
1666 |
1667 | // the regex is tested on the (up to) three chunks separately, and on substrings,
1668 | // so in order to have the correct offsets to check against okayToModify(), we
1669 | // have to keep track of how many characters are in the original source before
1670 | // the substring that we're looking at. Note that doLinkOrImage aligns the selection
1671 | // on potential brackets, so there should be no major breakage from the chunk
1672 | // separation.
1673 | var skippedChars = 0;
1674 |
1675 | var uniquified = complete.replace(regex, function uniquify(wholeMatch, before, inner, afterInner, id, end, offset) {
1676 | skippedChars += offset;
1677 | fakedefs += " [" + skippedChars + "]: " + testlink + skippedChars + "/unicorn\n";
1678 | skippedChars += before.length;
1679 | inner = inner.replace(regex, uniquify);
1680 | skippedChars -= before.length;
1681 | var result = before + inner + afterInner + skippedChars + end;
1682 | skippedChars -= offset;
1683 | return result;
1684 | });
1685 |
1686 | rendered = this.converter.makeHtml(uniquified + fakedefs);
1687 |
1688 | var okayToModify = function(offset) {
1689 | return rendered.indexOf(testlink + offset + "/unicorn") !== -1;
1690 | }
1691 |
1692 | var addDefNumber = function (def) {
1693 | refNumber++;
1694 | def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:");
1695 | defs += "\n" + def;
1696 | };
1697 |
1698 | // note that
1699 | // a) the recursive call to getLink cannot go infinite, because by definition
1700 | // of regex, inner is always a proper substring of wholeMatch, and
1701 | // b) more than one level of nesting is neither supported by the regex
1702 | // nor making a lot of sense (the only use case for nesting is a linked image)
1703 | var getLink = function (wholeMatch, before, inner, afterInner, id, end, offset) {
1704 | if (!okayToModify(skippedChars + offset))
1705 | return wholeMatch;
1706 | skippedChars += offset + before.length;
1707 | inner = inner.replace(regex, getLink);
1708 | skippedChars -= offset + before.length;
1709 | if (defsToAdd[id]) {
1710 | addDefNumber(defsToAdd[id]);
1711 | return before + inner + afterInner + refNumber + end;
1712 | }
1713 | return wholeMatch;
1714 | };
1715 |
1716 | var len = chunk.before.length;
1717 | chunk.before = chunk.before.replace(regex, getLink);
1718 | skippedChars += len;
1719 |
1720 | len = chunk.selection.length;
1721 | if (linkDef) {
1722 | addDefNumber(linkDef);
1723 | }
1724 | else {
1725 | chunk.selection = chunk.selection.replace(regex, getLink);
1726 | }
1727 | skippedChars += len;
1728 |
1729 | var refOut = refNumber;
1730 |
1731 | chunk.after = chunk.after.replace(regex, getLink);
1732 |
1733 | if (chunk.after) {
1734 | chunk.after = chunk.after.replace(/\n*$/, "");
1735 | }
1736 | if (!chunk.after) {
1737 | chunk.selection = chunk.selection.replace(/\n*$/, "");
1738 | }
1739 |
1740 | chunk.after += "\n\n" + defs;
1741 |
1742 | return refOut;
1743 | };
1744 |
1745 | // takes the line as entered into the add link/as image dialog and makes
1746 | // sure the URL and the optinal title are "nice".
1747 | function properlyEncoded(linkdef) {
1748 | return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
1749 |
1750 | var inQueryString = false;
1751 |
1752 | // Having `[^\w\d-./]` in there is just a shortcut that lets us skip
1753 | // the most common characters in URLs. Replacing that it with `.` would not change
1754 | // the result, because encodeURI returns those characters unchanged, but it
1755 | // would mean lots of unnecessary replacement calls. Having `[` and `]` in that
1756 | // section as well means we do *not* enocde square brackets. These characters are
1757 | // a strange beast in URLs, but if anything, this causes URLs to be more readable,
1758 | // and we leave it to the browser to make sure that these links are handled without
1759 | // problems.
1760 | link = link.replace(/%(?:[\da-fA-F]{2})|\?|\+|[^\w\d-./[\]]/g, function (match) {
1761 | // Valid percent encoding. Could just return it as is, but we follow RFC3986
1762 | // Section 2.1 which says "For consistency, URI producers and normalizers
1763 | // should use uppercase hexadecimal digits for all percent-encodings."
1764 | // Note that we also handle (illegal) stand-alone percent characters by
1765 | // replacing them with "%25"
1766 | if (match.length === 3 && match.charAt(0) == "%") {
1767 | return match.toUpperCase();
1768 | }
1769 | switch (match) {
1770 | case "?":
1771 | inQueryString = true;
1772 | return "?";
1773 | break;
1774 |
1775 | // In the query string, a plus and a space are identical -- normalize.
1776 | // Not strictly necessary, but identical behavior to the previous version
1777 | // of this function.
1778 | case "+":
1779 | if (inQueryString)
1780 | return "%20";
1781 | break;
1782 | }
1783 | return encodeURI(match);
1784 | })
1785 |
1786 | if (title) {
1787 | title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
1788 | title = title.replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">");
1789 | }
1790 | return title ? link + ' "' + title + '"' : link;
1791 | });
1792 | }
1793 |
1794 | commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
1795 |
1796 | chunk.trimWhitespace();
1797 | chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
1798 | var background;
1799 |
1800 | if (chunk.endTag.length > 1 && chunk.startTag.length > 0) {
1801 |
1802 | chunk.startTag = chunk.startTag.replace(/!?\[/, "");
1803 | chunk.endTag = "";
1804 | this.addLinkDef(chunk, null);
1805 |
1806 | }
1807 | else {
1808 |
1809 | // We're moving start and end tag back into the selection, since (as we're in the else block) we're not
1810 | // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
1811 | // link text. linkEnteredCallback takes care of escaping any brackets.
1812 | chunk.selection = chunk.startTag + chunk.selection + chunk.endTag;
1813 | chunk.startTag = chunk.endTag = "";
1814 |
1815 | if (/\n\n/.test(chunk.selection)) {
1816 | this.addLinkDef(chunk, null);
1817 | return;
1818 | }
1819 | var that = this;
1820 | // The function to be executed when you enter a link and press OK or Cancel.
1821 | // Marks up the link and adds the ref.
1822 | var linkEnteredCallback = function (link) {
1823 |
1824 | background.parentNode.removeChild(background);
1825 |
1826 | if (link !== null) {
1827 | // ( $1
1828 | // [^\\] anything that's not a backslash
1829 | // (?:\\\\)* an even number (this includes zero) of backslashes
1830 | // )
1831 | // (?= followed by
1832 | // [[\]] an opening or closing bracket
1833 | // )
1834 | //
1835 | // In other words, a non-escaped bracket. These have to be escaped now to make sure they
1836 | // don't count as the end of the link or similar.
1837 | // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets),
1838 | // the bracket in one match may be the "not a backslash" character in the next match, so it
1839 | // should not be consumed by the first match.
1840 | // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the
1841 | // start of the string, so this also works if the selection begins with a bracket. We cannot solve
1842 | // this by anchoring with ^, because in the case that the selection starts with two brackets, this
1843 | // would mean a zero-width match at the start. Since zero-width matches advance the string position,
1844 | // the first bracket could then not act as the "not a backslash" for the second.
1845 | chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
1846 |
1847 | var linkDef = " [999]: " + properlyEncoded(link);
1848 |
1849 | var num = that.addLinkDef(chunk, linkDef);
1850 | chunk.startTag = isImage ? "![" : "[";
1851 | chunk.endTag = "][" + num + "]";
1852 |
1853 | if (!chunk.selection) {
1854 | if (isImage) {
1855 | chunk.selection = that.getString("imagedescription");
1856 | }
1857 | else {
1858 | chunk.selection = that.getString("linkdescription");
1859 | }
1860 | }
1861 | }
1862 | postProcessing();
1863 | };
1864 |
1865 | background = ui.createBackground();
1866 |
1867 | if (isImage) {
1868 | if (!this.hooks.insertImageDialog(linkEnteredCallback))
1869 | ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback);
1870 | }
1871 | else {
1872 | ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback);
1873 | }
1874 | return true;
1875 | }
1876 | };
1877 |
1878 | // When making a list, hitting shift-enter will put your cursor on the next line
1879 | // at the current indent level.
1880 | commandProto.doAutoindent = function (chunk, postProcessing) {
1881 |
1882 | var commandMgr = this,
1883 | fakeSelection = false;
1884 |
1885 | chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
1886 | chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
1887 | chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
1888 |
1889 | // There's no selection, end the cursor wasn't at the end of the line:
1890 | // The user wants to split the current list item / code line / blockquote line
1891 | // (for the latter it doesn't really matter) in two. Temporarily select the
1892 | // (rest of the) line to achieve this.
1893 | if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
1894 | chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
1895 | chunk.selection = wholeMatch;
1896 | return "";
1897 | });
1898 | fakeSelection = true;
1899 | }
1900 |
1901 | if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
1902 | if (commandMgr.doList) {
1903 | commandMgr.doList(chunk);
1904 | }
1905 | }
1906 | if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
1907 | if (commandMgr.doBlockquote) {
1908 | commandMgr.doBlockquote(chunk);
1909 | }
1910 | }
1911 | if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
1912 | if (commandMgr.doCode) {
1913 | commandMgr.doCode(chunk);
1914 | }
1915 | }
1916 |
1917 | if (fakeSelection) {
1918 | chunk.after = chunk.selection + chunk.after;
1919 | chunk.selection = "";
1920 | }
1921 | };
1922 |
1923 | commandProto.doBlockquote = function (chunk, postProcessing) {
1924 |
1925 | chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
1926 | function (totalMatch, newlinesBefore, text, newlinesAfter) {
1927 | chunk.before += newlinesBefore;
1928 | chunk.after = newlinesAfter + chunk.after;
1929 | return text;
1930 | });
1931 |
1932 | chunk.before = chunk.before.replace(/(>[ \t]*)$/,
1933 | function (totalMatch, blankLine) {
1934 | chunk.selection = blankLine + chunk.selection;
1935 | return "";
1936 | });
1937 |
1938 | chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
1939 | chunk.selection = chunk.selection || this.getString("quoteexample");
1940 |
1941 | // The original code uses a regular expression to find out how much of the
1942 | // text *directly before* the selection already was a blockquote:
1943 |
1944 | /*
1945 | if (chunk.before) {
1946 | chunk.before = chunk.before.replace(/\n?$/, "\n");
1947 | }
1948 | chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
1949 | function (totalMatch) {
1950 | chunk.startTag = totalMatch;
1951 | return "";
1952 | });
1953 | */
1954 |
1955 | // This comes down to:
1956 | // Go backwards as many lines a possible, such that each line
1957 | // a) starts with ">", or
1958 | // b) is almost empty, except for whitespace, or
1959 | // c) is preceeded by an unbroken chain of non-empty lines
1960 | // leading up to a line that starts with ">" and at least one more character
1961 | // and in addition
1962 | // d) at least one line fulfills a)
1963 | //
1964 | // Since this is essentially a backwards-moving regex, it's susceptible to
1965 | // catstrophic backtracking and can cause the browser to hang;
1966 | // see e.g. http://meta.stackexchange.com/questions/9807.
1967 | //
1968 | // Hence we replaced this by a simple state machine that just goes through the
1969 | // lines and checks for a), b), and c).
1970 |
1971 | var match = "",
1972 | leftOver = "",
1973 | line;
1974 | if (chunk.before) {
1975 | var lines = chunk.before.replace(/\n$/, "").split("\n");
1976 | var inChain = false;
1977 | for (var i = 0; i < lines.length; i++) {
1978 | var good = false;
1979 | line = lines[i];
1980 | inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
1981 | if (/^>/.test(line)) { // a)
1982 | good = true;
1983 | if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain
1984 | inChain = true;
1985 | } else if (/^[ \t]*$/.test(line)) { // b)
1986 | good = true;
1987 | } else {
1988 | 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
1989 | }
1990 | if (good) {
1991 | match += line + "\n";
1992 | } else {
1993 | leftOver += match + line;
1994 | match = "\n";
1995 | }
1996 | }
1997 | if (!/(^|\n)>/.test(match)) { // d)
1998 | leftOver += match;
1999 | match = "";
2000 | }
2001 | }
2002 |
2003 | chunk.startTag = match;
2004 | chunk.before = leftOver;
2005 |
2006 | // end of change
2007 |
2008 | if (chunk.after) {
2009 | chunk.after = chunk.after.replace(/^\n?/, "\n");
2010 | }
2011 |
2012 | chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
2013 | function (totalMatch) {
2014 | chunk.endTag = totalMatch;
2015 | return "";
2016 | }
2017 | );
2018 |
2019 | var replaceBlanksInTags = function (useBracket) {
2020 |
2021 | var replacement = useBracket ? "> " : "";
2022 |
2023 | if (chunk.startTag) {
2024 | chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
2025 | function (totalMatch, markdown) {
2026 | return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
2027 | });
2028 | }
2029 | if (chunk.endTag) {
2030 | chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
2031 | function (totalMatch, markdown) {
2032 | return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
2033 | });
2034 | }
2035 | };
2036 |
2037 | if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
2038 | this.wrap(chunk, SETTINGS.lineLength - 2);
2039 | chunk.selection = chunk.selection.replace(/^/gm, "> ");
2040 | replaceBlanksInTags(true);
2041 | chunk.skipLines();
2042 | } else {
2043 | chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
2044 | this.unwrap(chunk);
2045 | replaceBlanksInTags(false);
2046 |
2047 | if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
2048 | chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
2049 | }
2050 |
2051 | if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
2052 | chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
2053 | }
2054 | }
2055 |
2056 | chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
2057 |
2058 | if (!/\n/.test(chunk.selection)) {
2059 | chunk.selection = chunk.selection.replace(/^(> *)/,
2060 | function (wholeMatch, blanks) {
2061 | chunk.startTag += blanks;
2062 | return "";
2063 | });
2064 | }
2065 | };
2066 |
2067 | commandProto.doCode = function (chunk, postProcessing) {
2068 |
2069 | var hasTextBefore = /\S[ ]*$/.test(chunk.before);
2070 | var hasTextAfter = /^[ ]*\S/.test(chunk.after);
2071 |
2072 | // Use 'four space' markdown if the selection is on its own
2073 | // line or is multiline.
2074 | if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
2075 |
2076 | chunk.before = chunk.before.replace(/[ ]{4}$/,
2077 | function (totalMatch) {
2078 | chunk.selection = totalMatch + chunk.selection;
2079 | return "";
2080 | });
2081 |
2082 | var nLinesBack = 1;
2083 | var nLinesForward = 1;
2084 |
2085 | if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
2086 | nLinesBack = 0;
2087 | }
2088 | if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
2089 | nLinesForward = 0;
2090 | }
2091 |
2092 | chunk.skipLines(nLinesBack, nLinesForward);
2093 |
2094 | if (!chunk.selection) {
2095 | chunk.startTag = " ";
2096 | chunk.selection = this.getString("codeexample");
2097 | }
2098 | else {
2099 | if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
2100 | if (/\n/.test(chunk.selection))
2101 | chunk.selection = chunk.selection.replace(/^/gm, " ");
2102 | else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
2103 | chunk.before += " ";
2104 | }
2105 | else {
2106 | chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, "");
2107 | }
2108 | }
2109 | }
2110 | else {
2111 | // Use backticks (`) to delimit the code block.
2112 |
2113 | chunk.trimWhitespace();
2114 | chunk.findTags(/`/, /`/);
2115 |
2116 | if (!chunk.startTag && !chunk.endTag) {
2117 | chunk.startTag = chunk.endTag = "`";
2118 | if (!chunk.selection) {
2119 | chunk.selection = this.getString("codeexample");
2120 | }
2121 | }
2122 | else if (chunk.endTag && !chunk.startTag) {
2123 | chunk.before += chunk.endTag;
2124 | chunk.endTag = "";
2125 | }
2126 | else {
2127 | chunk.startTag = chunk.endTag = "";
2128 | }
2129 | }
2130 | };
2131 |
2132 | commandProto.doList = function (chunk, postProcessing, isNumberedList) {
2133 |
2134 | // These are identical except at the very beginning and end.
2135 | // Should probably use the regex extension function to make this clearer.
2136 | var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
2137 | var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
2138 |
2139 | // The default bullet is a dash but others are possible.
2140 | // This has nothing to do with the particular HTML bullet,
2141 | // it's just a markdown bullet.
2142 | var bullet = "-";
2143 |
2144 | // The number in a numbered list.
2145 | var num = 1;
2146 |
2147 | // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
2148 | var getItemPrefix = function () {
2149 | var prefix;
2150 | if (isNumberedList) {
2151 | prefix = " " + num + ". ";
2152 | num++;
2153 | }
2154 | else {
2155 | prefix = " " + bullet + " ";
2156 | }
2157 | return prefix;
2158 | };
2159 |
2160 | // Fixes the prefixes of the other list items.
2161 | var getPrefixedItem = function (itemText) {
2162 |
2163 | // The numbering flag is unset when called by autoindent.
2164 | if (isNumberedList === undefined) {
2165 | isNumberedList = /^\s*\d/.test(itemText);
2166 | }
2167 |
2168 | // Renumber/bullet the list element.
2169 | itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
2170 | function (_) {
2171 | return getItemPrefix();
2172 | });
2173 |
2174 | return itemText;
2175 | };
2176 |
2177 | chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
2178 |
2179 | if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
2180 | chunk.before += chunk.startTag;
2181 | chunk.startTag = "";
2182 | }
2183 |
2184 | if (chunk.startTag) {
2185 |
2186 | var hasDigits = /\d+[.]/.test(chunk.startTag);
2187 | chunk.startTag = "";
2188 | chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
2189 | this.unwrap(chunk);
2190 | chunk.skipLines();
2191 |
2192 | if (hasDigits) {
2193 | // Have to renumber the bullet points if this is a numbered list.
2194 | chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
2195 | }
2196 | if (isNumberedList == hasDigits) {
2197 | return;
2198 | }
2199 | }
2200 |
2201 | var nLinesUp = 1;
2202 |
2203 | chunk.before = chunk.before.replace(previousItemsRegex,
2204 | function (itemText) {
2205 | if (/^\s*([*+-])/.test(itemText)) {
2206 | bullet = re.$1;
2207 | }
2208 | nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2209 | return getPrefixedItem(itemText);
2210 | });
2211 |
2212 | if (!chunk.selection) {
2213 | chunk.selection = this.getString("litem");
2214 | }
2215 |
2216 | var prefix = getItemPrefix();
2217 |
2218 | var nLinesDown = 1;
2219 |
2220 | chunk.after = chunk.after.replace(nextItemsRegex,
2221 | function (itemText) {
2222 | nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2223 | return getPrefixedItem(itemText);
2224 | });
2225 |
2226 | chunk.trimWhitespace(true);
2227 | chunk.skipLines(nLinesUp, nLinesDown, true);
2228 | chunk.startTag = prefix;
2229 | var spaces = prefix.replace(/./g, " ");
2230 | this.wrap(chunk, SETTINGS.lineLength - spaces.length);
2231 | chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
2232 |
2233 | };
2234 |
2235 | commandProto.doHeading = function (chunk, postProcessing) {
2236 |
2237 | // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
2238 | chunk.selection = chunk.selection.replace(/\s+/g, " ");
2239 | chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
2240 |
2241 | // If we clicked the button with no selected text, we just
2242 | // make a level 2 hash header around some default text.
2243 | if (!chunk.selection) {
2244 | chunk.startTag = "## ";
2245 | chunk.selection = this.getString("headingexample");
2246 | chunk.endTag = " ##";
2247 | return;
2248 | }
2249 |
2250 | var headerLevel = 0; // The existing header level of the selected text.
2251 |
2252 | // Remove any existing hash heading markdown and save the header level.
2253 | chunk.findTags(/#+[ ]*/, /[ ]*#+/);
2254 | if (/#+/.test(chunk.startTag)) {
2255 | headerLevel = re.lastMatch.length;
2256 | }
2257 | chunk.startTag = chunk.endTag = "";
2258 |
2259 | // Try to get the current header level by looking for - and = in the line
2260 | // below the selection.
2261 | chunk.findTags(null, /\s?(-+|=+)/);
2262 | if (/=+/.test(chunk.endTag)) {
2263 | headerLevel = 1;
2264 | }
2265 | if (/-+/.test(chunk.endTag)) {
2266 | headerLevel = 2;
2267 | }
2268 |
2269 | // Skip to the next line so we can create the header markdown.
2270 | chunk.startTag = chunk.endTag = "";
2271 | chunk.skipLines(1, 1);
2272 |
2273 | // We make a level 2 header if there is no current header.
2274 | // If there is a header level, we substract one from the header level.
2275 | // If it's already a level 1 header, it's removed.
2276 | var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
2277 |
2278 | if (headerLevelToCreate > 0) {
2279 |
2280 | // The button only creates level 1 and 2 underline headers.
2281 | // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner?
2282 | var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
2283 | var len = chunk.selection.length;
2284 | if (len > SETTINGS.lineLength) {
2285 | len = SETTINGS.lineLength;
2286 | }
2287 | chunk.endTag = "\n";
2288 | while (len--) {
2289 | chunk.endTag += headerChar;
2290 | }
2291 | }
2292 | };
2293 |
2294 | commandProto.doHorizontalRule = function (chunk, postProcessing) {
2295 | chunk.startTag = "----------\n";
2296 | chunk.selection = "";
2297 | chunk.skipLines(2, 1, true);
2298 | }
2299 |
2300 |
2301 | })();
--------------------------------------------------------------------------------
/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(?: start="\d+")?|p|pre|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 | function sanitizeTag(tag) {
31 | if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
32 | return tag;
33 | else
34 | return "";
35 | }
36 |
37 | ///
";
62 | var match;
63 | var tagpaired = [];
64 | var tagremove = [];
65 | var needsRemoval = false;
66 |
67 | // loop through matched tags in forward order
68 | for (var ctag = 0; ctag < tagcount; ctag++) {
69 | tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
70 | // skip any already paired tags
71 | // and skip tags in our ignore list; assume they're self-closed
72 | if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
73 | continue;
74 |
75 | tag = tags[ctag];
76 | match = -1;
77 |
78 | if (!/^<\//.test(tag)) {
79 | // this is an opening tag
80 | // search forwards (next tags), look for closing tags
81 | for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
82 | if (!tagpaired[ntag] && tags[ntag] == "" + tagname + ">") {
83 | match = ntag;
84 | break;
85 | }
86 | }
87 | }
88 |
89 | if (match == -1)
90 | needsRemoval = tagremove[ctag] = true; // mark for removal
91 | else
92 | tagpaired[match] = true; // mark paired
93 | }
94 |
95 | if (!needsRemoval)
96 | return html;
97 |
98 | // delete all orphaned tags from the string
99 |
100 | var ctag = 0;
101 | html = html.replace(re, function (match) {
102 | var res = tagremove[ctag] ? "" : match;
103 | ctag++;
104 | return res;
105 | });
106 | return html;
107 | }
108 | })();
109 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ujifgc/pagedown/39d7468a82f7462177df2870449135cf315f8d25/README.txt
--------------------------------------------------------------------------------
/demo/browser/demo.css:
--------------------------------------------------------------------------------
1 | body
2 | {
3 | background-color: White;
4 | font-family: sans-serif;
5 | }
6 |
7 | blockquote {
8 | border-left: 2px dotted #888;
9 | padding-left: 5px;
10 | background: #d0f0ff;
11 | }
12 |
13 | .wmd-panel
14 | {
15 | margin-left: 25%;
16 | margin-right: 25%;
17 | width: 50%;
18 | min-width: 500px;
19 | }
20 |
21 | .wmd-button-bar
22 | {
23 | width: 100%;
24 | background-color: Silver;
25 | }
26 |
27 | .wmd-input
28 | {
29 | height: 300px;
30 | width: 100%;
31 | background-color: Gainsboro;
32 | border: 1px solid DarkGray;
33 | }
34 |
35 | .wmd-preview
36 | {
37 | background-color: #c0e0ff;
38 | }
39 |
40 | .wmd-button-row
41 | {
42 | position: relative;
43 | margin-left: 5px;
44 | margin-right: 5px;
45 | margin-bottom: 5px;
46 | margin-top: 10px;
47 | padding: 0px;
48 | height: 20px;
49 | }
50 |
51 | .wmd-spacer
52 | {
53 | width: 1px;
54 | height: 20px;
55 | margin-left: 14px;
56 |
57 | position: absolute;
58 | background-color: Silver;
59 | display: inline-block;
60 | list-style: none;
61 | }
62 |
63 | .wmd-button {
64 | width: 20px;
65 | height: 20px;
66 | padding-left: 2px;
67 | padding-right: 3px;
68 | position: absolute;
69 | display: inline-block;
70 | list-style: none;
71 | cursor: pointer;
72 | }
73 |
74 | .wmd-button > span {
75 | background-image: url(../../wmd-buttons.png);
76 | background-repeat: no-repeat;
77 | background-position: 0px 0px;
78 | width: 20px;
79 | height: 20px;
80 | display: inline-block;
81 | }
82 |
83 | .wmd-spacer1
84 | {
85 | left: 50px;
86 | }
87 | .wmd-spacer2
88 | {
89 | left: 175px;
90 | }
91 | .wmd-spacer3
92 | {
93 | left: 300px;
94 | }
95 |
96 |
97 |
98 |
99 | .wmd-prompt-background
100 | {
101 | background-color: Black;
102 | }
103 |
104 | .wmd-prompt-dialog
105 | {
106 | border: 1px solid #999999;
107 | background-color: #F5F5F5;
108 | }
109 |
110 | .wmd-prompt-dialog > div {
111 | font-size: 0.8em;
112 | font-family: arial, helvetica, sans-serif;
113 | }
114 |
115 |
116 | .wmd-prompt-dialog > form > input[type="text"] {
117 | border: 1px solid #999999;
118 | color: black;
119 | }
120 |
121 | .wmd-prompt-dialog > form > input[type="button"]{
122 | border: 1px solid #888888;
123 | font-family: trebuchet MS, helvetica, sans-serif;
124 | font-size: 0.8em;
125 | font-weight: bold;
126 | }
127 |
--------------------------------------------------------------------------------
/demo/browser/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
40 |
41 | Your output, sanitized:
\n" + saneConv.makeHtml(markdown))
30 | res.write("Your output, unsanitized:
\n" + conv.makeHtml(markdown))
31 |
32 | res.write(
33 | "Enter Markdown
\n" +
34 | ""
40 | );
41 |
42 | res.end("");
43 |
44 | }).listen(8000);
45 |
--------------------------------------------------------------------------------
/local/Markdown.local.fr.js:
--------------------------------------------------------------------------------
1 | // Usage:
2 | //
3 | // var myConverter = new Markdown.Editor(myConverter, null, { strings: Markdown.local.fr });
4 |
5 | (function () {
6 | Markdown.local = Markdown.local || {};
7 | Markdown.local.fr = {
8 | bold: "Gras Ctrl+B",
9 | boldexample: "texte en gras",
10 |
11 | italic: "Italique Ctrl+I",
12 | italicexample: "texte en italique",
13 |
14 | link: "Hyperlien Ctrl+L",
15 | linkdescription: "description de l'hyperlien",
16 | linkdialog: " Ctrl+Q",
19 | quoteexample: "Citation",
20 |
21 | code: "Extrait de code
Ctrl+K",
22 | codeexample: "votre extrait de code",
23 |
24 | image: "Image
Ctrl+G",
25 | imagedescription: "description de l'image",
26 | imagedialog: "
Vous chercher un hébergement d'image grauit ? Ctrl+O",
29 | ulist: "Liste à point
Ctrl+U",
30 | litem: "Elément de liste",
31 |
32 | heading: "Titre
/
Ctrl+H",
33 | headingexample: "Titre",
34 |
35 | hr: "Trait horizontal
Ctrl+R",
36 |
37 | undo: "Annuler - Ctrl+Z",
38 | redo: "Refaire - Ctrl+Y",
39 | redomac: "Refaire - Ctrl+Shift+Z",
40 |
41 | help: "Aide sur Markdown"
42 | };
43 | })();
--------------------------------------------------------------------------------
/node-pagedown.js:
--------------------------------------------------------------------------------
1 | exports.Converter = require("./Markdown.Converter").Converter;
2 | exports.getSanitizingConverter = require("./Markdown.Sanitizer").getSanitizingConverter;
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pagedown",
3 | "version": "1.1.0",
4 | "description": "markdown converter, based on showdown",
5 | "repository": { "type": "hg", "url": "https://code.google.com/p/pagedown/" },
6 | "keywords": ["markdown"],
7 | "license": "MIT",
8 | "files": ["Markdown.Converter.js", "Markdown.Sanitizer.js", "node-pagedown.js"],
9 | "main": "node-pagedown.js",
10 | "bugs": "http://code.google.com/p/pagedown/issues/list",
11 | "homepage": "http://code.google.com/p/pagedown/wiki/PageDown"
12 | }
13 |
--------------------------------------------------------------------------------
/resources/wmd-buttons.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ujifgc/pagedown/39d7468a82f7462177df2870449135cf315f8d25/resources/wmd-buttons.psd
--------------------------------------------------------------------------------
/wmd-buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ujifgc/pagedown/39d7468a82f7462177df2870449135cf315f8d25/wmd-buttons.png
--------------------------------------------------------------------------------