markdown.js 131 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671
  1. var Markdown;
  2. if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module
  3. Markdown = exports;
  4. else
  5. Markdown = {};
  6. // The following text is included for historical reasons, but should
  7. // be taken with a pinch of salt; it's not all true anymore.
  8. //
  9. // Wherever possible, Showdown is a straight, line-by-line port
  10. // of the Perl version of Markdown.
  11. //
  12. // This is not a normal parser design; it's basically just a
  13. // series of string substitutions. It's hard to read and
  14. // maintain this way, but keeping Showdown close to the original
  15. // design makes it easier to port new features.
  16. //
  17. // More importantly, Showdown behaves like markdown.pl in most
  18. // edge cases. So web applications can do client-side preview
  19. // in Javascript, and then build identical HTML on the server.
  20. //
  21. // This port needs the new RegExp functionality of ECMA 262,
  22. // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers
  23. // should do fine. Even with the new regular expression features,
  24. // We do a lot of work to emulate Perl's regex functionality.
  25. // The tricky changes in this file mostly have the "attacklab:"
  26. // label. Major or self-explanatory changes don't.
  27. //
  28. // Smart diff tools like Araxis Merge will be able to match up
  29. // this file with markdown.pl in a useful way. A little tweaking
  30. // helps: in a copy of markdown.pl, replace "#" with "//" and
  31. // replace "$text" with "text". Be sure to ignore whitespace
  32. // and line endings.
  33. //
  34. //
  35. // Usage:
  36. //
  37. // var text = "Markdown *rocks*.";
  38. //
  39. // var converter = new Markdown.Converter();
  40. // var html = converter.makeHtml(text);
  41. //
  42. // alert(html);
  43. //
  44. // Note: move the sample code to the bottom of this
  45. // file before uncommenting it.
  46. //
  47. (function () {
  48. function identity(x) { return x; }
  49. function returnFalse(x) { return false; }
  50. function HookCollection() { }
  51. HookCollection.prototype = {
  52. chain: function (hookname, func) {
  53. var original = this[hookname];
  54. if (!original)
  55. throw new Error("unknown hook " + hookname);
  56. if (original === identity)
  57. this[hookname] = func;
  58. else
  59. this[hookname] = function (x) { return func(original(x)); }
  60. },
  61. set: function (hookname, func) {
  62. if (!this[hookname])
  63. throw new Error("unknown hook " + hookname);
  64. this[hookname] = func;
  65. },
  66. addNoop: function (hookname) {
  67. this[hookname] = identity;
  68. },
  69. addFalse: function (hookname) {
  70. this[hookname] = returnFalse;
  71. }
  72. };
  73. Markdown.HookCollection = HookCollection;
  74. // g_urls and g_titles allow arbitrary user-entered strings as keys. This
  75. // caused an exception (and hence stopped the rendering) when the user entered
  76. // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this
  77. // (since no builtin property starts with "s_"). See
  78. // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug
  79. // (granted, switching from Array() to Object() alone would have left only __proto__
  80. // to be a problem)
  81. function SaveHash() { }
  82. SaveHash.prototype = {
  83. set: function (key, value) {
  84. this["s_" + key] = value;
  85. },
  86. get: function (key) {
  87. return this["s_" + key];
  88. }
  89. };
  90. Markdown.Converter = function () {
  91. var pluginHooks = this.hooks = new HookCollection();
  92. pluginHooks.addNoop("plainLinkText"); // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link
  93. pluginHooks.addNoop("preConversion"); // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked
  94. pluginHooks.addNoop("postConversion"); // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml
  95. //
  96. // Private state of the converter instance:
  97. //
  98. // Global hashes, used by various utility routines
  99. var g_urls;
  100. var g_titles;
  101. var g_html_blocks;
  102. // Used to track when we're inside an ordered or unordered list
  103. // (see _ProcessListItems() for details):
  104. var g_list_level;
  105. this.makeHtml = function (text) {
  106. //
  107. // Main function. The order in which other subs are called here is
  108. // essential. Link and image substitutions need to happen before
  109. // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the <a>
  110. // and <img> tags get encoded.
  111. //
  112. // This will only happen if makeHtml on the same converter instance is called from a plugin hook.
  113. // Don't do that.
  114. if (g_urls)
  115. throw new Error("Recursive call to converter.makeHtml");
  116. // Create the private state objects.
  117. g_urls = new SaveHash();
  118. g_titles = new SaveHash();
  119. g_html_blocks = [];
  120. g_list_level = 0;
  121. text = pluginHooks.preConversion(text);
  122. // attacklab: Replace ~ with ~T
  123. // This lets us use tilde as an escape char to avoid md5 hashes
  124. // The choice of character is arbitray; anything that isn't
  125. // magic in Markdown will work.
  126. text = text.replace(/~/g, "~T");
  127. // attacklab: Replace $ with ~D
  128. // RegExp interprets $ as a special character
  129. // when it's in a replacement string
  130. text = text.replace(/\$/g, "~D");
  131. // Standardize line endings
  132. text = text.replace(/\r\n/g, "\n"); // DOS to Unix
  133. text = text.replace(/\r/g, "\n"); // Mac to Unix
  134. // Make sure text begins and ends with a couple of newlines:
  135. text = "\n\n" + text + "\n\n";
  136. // Convert all tabs to spaces.
  137. text = _Detab(text);
  138. // Strip any lines consisting only of spaces and tabs.
  139. // This makes subsequent regexen easier to write, because we can
  140. // match consecutive blank lines with /\n+/ instead of something
  141. // contorted like /[ \t]*\n+/ .
  142. text = text.replace(/^[ \t]+$/mg, "");
  143. // Turn block-level HTML blocks into hash entries
  144. text = _HashHTMLBlocks(text);
  145. // Strip link definitions, store in hashes.
  146. text = _StripLinkDefinitions(text);
  147. text = _RunBlockGamut(text);
  148. text = _UnescapeSpecialChars(text);
  149. // attacklab: Restore dollar signs
  150. text = text.replace(/~D/g, "$$");
  151. // attacklab: Restore tildes
  152. text = text.replace(/~T/g, "~");
  153. text = pluginHooks.postConversion(text);
  154. g_html_blocks = g_titles = g_urls = null;
  155. return text;
  156. };
  157. function _StripLinkDefinitions(text) {
  158. //
  159. // Strips link definitions from text, stores the URLs and titles in
  160. // hash references.
  161. //
  162. // Link defs are in the form: ^[id]: url "optional title"
  163. /*
  164. text = text.replace(/
  165. ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1
  166. [ \t]*
  167. \n? // maybe *one* newline
  168. [ \t]*
  169. <?(\S+?)>? // url = $2
  170. (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below
  171. [ \t]*
  172. \n? // maybe one newline
  173. [ \t]*
  174. ( // (potential) title = $3
  175. (\n*) // any lines skipped = $4 attacklab: lookbehind removed
  176. [ \t]+
  177. ["(]
  178. (.+?) // title = $5
  179. [")]
  180. [ \t]*
  181. )? // title is optional
  182. (?:\n+|$)
  183. /gm, function(){...});
  184. */
  185. text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm,
  186. function (wholeMatch, m1, m2, m3, m4, m5) {
  187. m1 = m1.toLowerCase();
  188. g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive
  189. if (m4) {
  190. // Oops, found blank lines, so it's not a title.
  191. // Put back the parenthetical statement we stole.
  192. return m3;
  193. } else if (m5) {
  194. g_titles.set(m1, m5.replace(/"/g, "&quot;"));
  195. }
  196. // Completely remove the definition from the text
  197. return "";
  198. }
  199. );
  200. return text;
  201. }
  202. function _HashHTMLBlocks(text) {
  203. // Hashify HTML blocks:
  204. // We only want to do this for block-level HTML tags, such as headers,
  205. // lists, and tables. That's because we still want to wrap <p>s around
  206. // "paragraphs" that are wrapped in non-block-level tags, such as anchors,
  207. // phrase emphasis, and spans. The list of tags we're looking for is
  208. // hard-coded:
  209. var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del"
  210. var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math"
  211. // First, look for nested blocks, e.g.:
  212. // <div>
  213. // <div>
  214. // tags for inner block must be indented.
  215. // </div>
  216. // </div>
  217. //
  218. // The outermost tags must start at the left margin for this to match, and
  219. // the inner nested divs must be indented.
  220. // We need to do this before the next, more liberal match, because the next
  221. // match will start at the first `<div>` and stop at the first `</div>`.
  222. // attacklab: This regex can be expensive when it fails.
  223. /*
  224. text = text.replace(/
  225. ( // save in $1
  226. ^ // start of line (with /m)
  227. <($block_tags_a) // start tag = $2
  228. \b // word break
  229. // attacklab: hack around khtml/pcre bug...
  230. [^\r]*?\n // any number of lines, minimally matching
  231. </\2> // the matching end tag
  232. [ \t]* // trailing spaces/tabs
  233. (?=\n+) // followed by a newline
  234. ) // attacklab: there are sentinel newlines at end of document
  235. /gm,function(){...}};
  236. */
  237. text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement);
  238. //
  239. // Now match more liberally, simply from `\n<tag>` to `</tag>\n`
  240. //
  241. /*
  242. text = text.replace(/
  243. ( // save in $1
  244. ^ // start of line (with /m)
  245. <($block_tags_b) // start tag = $2
  246. \b // word break
  247. // attacklab: hack around khtml/pcre bug...
  248. [^\r]*? // any number of lines, minimally matching
  249. .*</\2> // the matching end tag
  250. [ \t]* // trailing spaces/tabs
  251. (?=\n+) // followed by a newline
  252. ) // attacklab: there are sentinel newlines at end of document
  253. /gm,function(){...}};
  254. */
  255. text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement);
  256. // Special case just for <hr />. It was easier to make a special case than
  257. // to make the other regex more complicated.
  258. /*
  259. text = text.replace(/
  260. \n // Starting after a blank line
  261. [ ]{0,3}
  262. ( // save in $1
  263. (<(hr) // start tag = $2
  264. \b // word break
  265. ([^<>])*?
  266. \/?>) // the matching end tag
  267. [ \t]*
  268. (?=\n{2,}) // followed by a blank line
  269. )
  270. /g,hashElement);
  271. */
  272. text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement);
  273. // Special case for standalone HTML comments:
  274. /*
  275. text = text.replace(/
  276. \n\n // Starting after a blank line
  277. [ ]{0,3} // attacklab: g_tab_width - 1
  278. ( // save in $1
  279. <!
  280. (--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256
  281. >
  282. [ \t]*
  283. (?=\n{2,}) // followed by a blank line
  284. )
  285. /g,hashElement);
  286. */
  287. text = text.replace(/\n\n[ ]{0,3}(<!(--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement);
  288. // PHP and ASP-style processor instructions (<?...?> and <%...%>)
  289. /*
  290. text = text.replace(/
  291. (?:
  292. \n\n // Starting after a blank line
  293. )
  294. ( // save in $1
  295. [ ]{0,3} // attacklab: g_tab_width - 1
  296. (?:
  297. <([?%]) // $2
  298. [^\r]*?
  299. \2>
  300. )
  301. [ \t]*
  302. (?=\n{2,}) // followed by a blank line
  303. )
  304. /g,hashElement);
  305. */
  306. text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement);
  307. return text;
  308. }
  309. function hashElement(wholeMatch, m1) {
  310. var blockText = m1;
  311. // Undo double lines
  312. blockText = blockText.replace(/^\n+/, "");
  313. // strip trailing blank lines
  314. blockText = blockText.replace(/\n+$/g, "");
  315. // Replace the element text with a marker ("~KxK" where x is its key)
  316. blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n";
  317. return blockText;
  318. }
  319. function _RunBlockGamut(text, doNotUnhash) {
  320. //
  321. // These are all the transformations that form block-level
  322. // tags like paragraphs, headers, and list items.
  323. //
  324. text = _DoHeaders(text);
  325. // Do Horizontal Rules:
  326. var replacement = "<hr />\n";
  327. text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement);
  328. text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement);
  329. text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement);
  330. text = _DoLists(text);
  331. text = _DoCodeBlocks(text);
  332. text = _DoBlockQuotes(text);
  333. // We already ran _HashHTMLBlocks() before, in Markdown(), but that
  334. // was to escape raw HTML in the original Markdown source. This time,
  335. // we're escaping the markup we've just created, so that we don't wrap
  336. // <p> tags around block-level tags.
  337. text = _HashHTMLBlocks(text);
  338. text = _FormParagraphs(text, doNotUnhash);
  339. return text;
  340. }
  341. function _RunSpanGamut(text) {
  342. //
  343. // These are all the transformations that occur *within* block-level
  344. // tags like paragraphs, headers, and list items.
  345. //
  346. text = _DoCodeSpans(text);
  347. text = _EscapeSpecialCharsWithinTagAttributes(text);
  348. text = _EncodeBackslashEscapes(text);
  349. // Process anchor and image tags. Images must come first,
  350. // because ![foo][f] looks like an anchor.
  351. text = _DoImages(text);
  352. text = _DoAnchors(text);
  353. // Make links out of things like `<http://example.com/>`
  354. // Must come after _DoAnchors(), because you can use < and >
  355. // delimiters in inline links like [this](<url>).
  356. text = _DoAutoLinks(text);
  357. text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now
  358. text = _EncodeAmpsAndAngles(text);
  359. text = _DoItalicsAndBold(text);
  360. // Do hard breaks:
  361. text = text.replace(/ +\n/g, " <br>\n");
  362. return text;
  363. }
  364. function _EscapeSpecialCharsWithinTagAttributes(text) {
  365. //
  366. // Within tags -- meaning between < and > -- encode [\ ` * _] so they
  367. // don't conflict with their use in Markdown for code, italics and strong.
  368. //
  369. // Build a regex to find HTML tags and comments. See Friedl's
  370. // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
  371. // SE: changed the comment part of the regex
  372. var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi;
  373. text = text.replace(regex, function (wholeMatch) {
  374. var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`");
  375. tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987
  376. return tag;
  377. });
  378. return text;
  379. }
  380. function _DoAnchors(text) {
  381. //
  382. // Turn Markdown link shortcuts into XHTML <a> tags.
  383. //
  384. //
  385. // First, handle reference-style links: [link text] [id]
  386. //
  387. /*
  388. text = text.replace(/
  389. ( // wrap whole match in $1
  390. \[
  391. (
  392. (?:
  393. \[[^\]]*\] // allow brackets nested one level
  394. |
  395. [^\[] // or anything else
  396. )*
  397. )
  398. \]
  399. [ ]? // one optional space
  400. (?:\n[ ]*)? // one optional newline followed by spaces
  401. \[
  402. (.*?) // id = $3
  403. \]
  404. )
  405. ()()()() // pad remaining backreferences
  406. /g, writeAnchorTag);
  407. */
  408. text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);
  409. //
  410. // Next, inline-style links: [link text](url "optional title")
  411. //
  412. /*
  413. text = text.replace(/
  414. ( // wrap whole match in $1
  415. \[
  416. (
  417. (?:
  418. \[[^\]]*\] // allow brackets nested one level
  419. |
  420. [^\[\]] // or anything else
  421. )*
  422. )
  423. \]
  424. \( // literal paren
  425. [ \t]*
  426. () // no id, so leave $3 empty
  427. <?( // href = $4
  428. (?:
  429. \([^)]*\) // allow one level of (correctly nested) parens (think MSDN)
  430. |
  431. [^()]
  432. )*?
  433. )>?
  434. [ \t]*
  435. ( // $5
  436. (['"]) // quote char = $6
  437. (.*?) // Title = $7
  438. \6 // matching quote
  439. [ \t]* // ignore any spaces/tabs between closing quote and )
  440. )? // title is optional
  441. \)
  442. )
  443. /g, writeAnchorTag);
  444. */
  445. text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?((?:\([^)]*\)|[^()])*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag);
  446. //
  447. // Last, handle reference-style shortcuts: [link text]
  448. // These must come last in case you've also got [link test][1]
  449. // or [link test](/foo)
  450. //
  451. /*
  452. text = text.replace(/
  453. ( // wrap whole match in $1
  454. \[
  455. ([^\[\]]+) // link text = $2; can't contain '[' or ']'
  456. \]
  457. )
  458. ()()()()() // pad rest of backreferences
  459. /g, writeAnchorTag);
  460. */
  461. text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
  462. return text;
  463. }
  464. function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
  465. if (m7 == undefined) m7 = "";
  466. var whole_match = m1;
  467. var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs
  468. var link_id = m3.toLowerCase();
  469. var url = m4;
  470. var title = m7;
  471. if (url == "") {
  472. if (link_id == "") {
  473. // lower-case and turn embedded newlines into spaces
  474. link_id = link_text.toLowerCase().replace(/ ?\n/g, " ");
  475. }
  476. url = "#" + link_id;
  477. if (g_urls.get(link_id) != undefined) {
  478. url = g_urls.get(link_id);
  479. if (g_titles.get(link_id) != undefined) {
  480. title = g_titles.get(link_id);
  481. }
  482. }
  483. else {
  484. if (whole_match.search(/\(\s*\)$/m) > -1) {
  485. // Special case for explicit empty url
  486. url = "";
  487. } else {
  488. return whole_match;
  489. }
  490. }
  491. }
  492. url = encodeProblemUrlChars(url);
  493. url = escapeCharacters(url, "*_");
  494. var result = "<a href=\"" + url + "\"";
  495. if (title != "") {
  496. title = attributeEncode(title);
  497. title = escapeCharacters(title, "*_");
  498. result += " title=\"" + title + "\"";
  499. }
  500. result += ">" + link_text + "</a>";
  501. return result;
  502. }
  503. function _DoImages(text) {
  504. //
  505. // Turn Markdown image shortcuts into <img> tags.
  506. //
  507. //
  508. // First, handle reference-style labeled images: ![alt text][id]
  509. //
  510. /*
  511. text = text.replace(/
  512. ( // wrap whole match in $1
  513. !\[
  514. (.*?) // alt text = $2
  515. \]
  516. [ ]? // one optional space
  517. (?:\n[ ]*)? // one optional newline followed by spaces
  518. \[
  519. (.*?) // id = $3
  520. \]
  521. )
  522. ()()()() // pad rest of backreferences
  523. /g, writeImageTag);
  524. */
  525. text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag);
  526. //
  527. // Next, handle inline images: ![alt text](url "optional title")
  528. // Don't forget: encode * and _
  529. /*
  530. text = text.replace(/
  531. ( // wrap whole match in $1
  532. !\[
  533. (.*?) // alt text = $2
  534. \]
  535. \s? // One optional whitespace character
  536. \( // literal paren
  537. [ \t]*
  538. () // no id, so leave $3 empty
  539. <?(\S+?)>? // src url = $4
  540. [ \t]*
  541. ( // $5
  542. (['"]) // quote char = $6
  543. (.*?) // title = $7
  544. \6 // matching quote
  545. [ \t]*
  546. )? // title is optional
  547. \)
  548. )
  549. /g, writeImageTag);
  550. */
  551. text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()<?(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag);
  552. return text;
  553. }
  554. function attributeEncode(text) {
  555. // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title)
  556. // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it)
  557. return text.replace(/>/g, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
  558. }
  559. function writeImageTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
  560. var whole_match = m1;
  561. var alt_text = m2;
  562. var link_id = m3.toLowerCase();
  563. var url = m4;
  564. var title = m7;
  565. if (!title) title = "";
  566. if (url == "") {
  567. if (link_id == "") {
  568. // lower-case and turn embedded newlines into spaces
  569. link_id = alt_text.toLowerCase().replace(/ ?\n/g, " ");
  570. }
  571. url = "#" + link_id;
  572. if (g_urls.get(link_id) != undefined) {
  573. url = g_urls.get(link_id);
  574. if (g_titles.get(link_id) != undefined) {
  575. title = g_titles.get(link_id);
  576. }
  577. }
  578. else {
  579. return whole_match;
  580. }
  581. }
  582. alt_text = escapeCharacters(attributeEncode(alt_text), "*_[]()");
  583. url = escapeCharacters(url, "*_");
  584. var result = "<img src=\"" + url + "\" alt=\"" + alt_text + "\"";
  585. // attacklab: Markdown.pl adds empty title attributes to images.
  586. // Replicate this bug.
  587. //if (title != "") {
  588. title = attributeEncode(title);
  589. title = escapeCharacters(title, "*_");
  590. result += " title=\"" + title + "\"";
  591. //}
  592. result += " />";
  593. return result;
  594. }
  595. function _DoHeaders(text) {
  596. // Setext-style headers:
  597. // Header 1
  598. // ========
  599. //
  600. // Header 2
  601. // --------
  602. //
  603. text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
  604. function (wholeMatch, m1) { return "<h1>" + _RunSpanGamut(m1) + "</h1>\n\n"; }
  605. );
  606. text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
  607. function (matchFound, m1) { return "<h2>" + _RunSpanGamut(m1) + "</h2>\n\n"; }
  608. );
  609. // atx-style headers:
  610. // # Header 1
  611. // ## Header 2
  612. // ## Header 2 with closing hashes ##
  613. // ...
  614. // ###### Header 6
  615. //
  616. /*
  617. text = text.replace(/
  618. ^(\#{1,6}) // $1 = string of #'s
  619. [ \t]*
  620. (.+?) // $2 = Header text
  621. [ \t]*
  622. \#* // optional closing #'s (not counted)
  623. \n+
  624. /gm, function() {...});
  625. */
  626. text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
  627. function (wholeMatch, m1, m2) {
  628. var h_level = m1.length;
  629. return "<h" + h_level + ">" + _RunSpanGamut(m2) + "</h" + h_level + ">\n\n";
  630. }
  631. );
  632. return text;
  633. }
  634. function _DoLists(text) {
  635. //
  636. // Form HTML ordered (numbered) and unordered (bulleted) lists.
  637. //
  638. // attacklab: add sentinel to hack around khtml/safari bug:
  639. // http://bugs.webkit.org/show_bug.cgi?id=11231
  640. text += "~0";
  641. // Re-usable pattern to match any entirel ul or ol list:
  642. /*
  643. var whole_list = /
  644. ( // $1 = whole list
  645. ( // $2
  646. [ ]{0,3} // attacklab: g_tab_width - 1
  647. ([*+-]|\d+[.]) // $3 = first list item marker
  648. [ \t]+
  649. )
  650. [^\r]+?
  651. ( // $4
  652. ~0 // sentinel for workaround; should be $
  653. |
  654. \n{2,}
  655. (?=\S)
  656. (?! // Negative lookahead for another list item marker
  657. [ \t]*
  658. (?:[*+-]|\d+[.])[ \t]+
  659. )
  660. )
  661. )
  662. /g
  663. */
  664. var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
  665. if (g_list_level) {
  666. text = text.replace(whole_list, function (wholeMatch, m1, m2) {
  667. var list = m1;
  668. var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol";
  669. var result = _ProcessListItems(list, list_type);
  670. // Trim any trailing whitespace, to put the closing `</$list_type>`
  671. // up on the preceding line, to get it past the current stupid
  672. // HTML block parser. This is a hack to work around the terrible
  673. // hack that is the HTML block parser.
  674. result = result.replace(/\s+$/, "");
  675. result = "<" + list_type + ">" + result + "</" + list_type + ">\n";
  676. return result;
  677. });
  678. } else {
  679. whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;
  680. text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) {
  681. var runup = m1;
  682. var list = m2;
  683. var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol";
  684. var result = _ProcessListItems(list, list_type);
  685. result = runup + "<" + list_type + ">\n" + result + "</" + list_type + ">\n";
  686. return result;
  687. });
  688. }
  689. // attacklab: strip sentinel
  690. text = text.replace(/~0/, "");
  691. return text;
  692. }
  693. var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" };
  694. function _ProcessListItems(list_str, list_type) {
  695. //
  696. // Process the contents of a single ordered or unordered list, splitting it
  697. // into individual list items.
  698. //
  699. // list_type is either "ul" or "ol".
  700. // The $g_list_level global keeps track of when we're inside a list.
  701. // Each time we enter a list, we increment it; when we leave a list,
  702. // we decrement. If it's zero, we're not in a list anymore.
  703. //
  704. // We do this because when we're not inside a list, we want to treat
  705. // something like this:
  706. //
  707. // I recommend upgrading to version
  708. // 8. Oops, now this line is treated
  709. // as a sub-list.
  710. //
  711. // As a single paragraph, despite the fact that the second line starts
  712. // with a digit-period-space sequence.
  713. //
  714. // Whereas when we're inside a list (or sub-list), that line will be
  715. // treated as the start of a sub-list. What a kludge, huh? This is
  716. // an aspect of Markdown's syntax that's hard to parse perfectly
  717. // without resorting to mind-reading. Perhaps the solution is to
  718. // change the syntax rules such that sub-lists must start with a
  719. // starting cardinal number; e.g. "1." or "a.".
  720. g_list_level++;
  721. // trim trailing blank lines:
  722. list_str = list_str.replace(/\n{2,}$/, "\n");
  723. // attacklab: add sentinel to emulate \z
  724. list_str += "~0";
  725. // In the original attacklab showdown, list_type was not given to this function, and anything
  726. // that matched /[*+-]|\d+[.]/ would just create the next <li>, causing this mismatch:
  727. //
  728. // Markdown rendered by WMD rendered by MarkdownSharp
  729. // ------------------------------------------------------------------
  730. // 1. first 1. first 1. first
  731. // 2. second 2. second 2. second
  732. // - third 3. third * third
  733. //
  734. // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx,
  735. // with {MARKER} being one of \d+[.] or [*+-], depending on list_type:
  736. /*
  737. list_str = list_str.replace(/
  738. (^[ \t]*) // leading whitespace = $1
  739. ({MARKER}) [ \t]+ // list marker = $2
  740. ([^\r]+? // list item text = $3
  741. (\n+)
  742. )
  743. (?=
  744. (~0 | \2 ({MARKER}) [ \t]+)
  745. )
  746. /gm, function(){...});
  747. */
  748. var marker = _listItemMarkers[list_type];
  749. var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm");
  750. var last_item_had_a_double_newline = false;
  751. list_str = list_str.replace(re,
  752. function (wholeMatch, m1, m2, m3) {
  753. var item = m3;
  754. var leading_space = m1;
  755. var ends_with_double_newline = /\n\n$/.test(item);
  756. var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1;
  757. if (contains_double_newline || last_item_had_a_double_newline) {
  758. item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true);
  759. }
  760. else {
  761. // Recursion for sub-lists:
  762. item = _DoLists(_Outdent(item));
  763. item = item.replace(/\n$/, ""); // chomp(item)
  764. item = _RunSpanGamut(item);
  765. }
  766. last_item_had_a_double_newline = ends_with_double_newline;
  767. return "<li>" + item + "</li>\n";
  768. }
  769. );
  770. // attacklab: strip sentinel
  771. list_str = list_str.replace(/~0/g, "");
  772. g_list_level--;
  773. return list_str;
  774. }
  775. function _DoCodeBlocks(text) {
  776. //
  777. // Process Markdown `<pre><code>` blocks.
  778. //
  779. /*
  780. text = text.replace(/
  781. (?:\n\n|^)
  782. ( // $1 = the code block -- one or more lines, starting with a space/tab
  783. (?:
  784. (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
  785. .*\n+
  786. )+
  787. )
  788. (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
  789. /g ,function(){...});
  790. */
  791. // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
  792. text += "~0";
  793. text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
  794. function (wholeMatch, m1, m2) {
  795. var codeblock = m1;
  796. var nextChar = m2;
  797. codeblock = _EncodeCode(_Outdent(codeblock));
  798. codeblock = _Detab(codeblock);
  799. codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
  800. codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
  801. codeblock = '<pre class="prettyprint linenums"><code>' + codeblock + '\n</code></pre>';
  802. return "\n\n" + codeblock + "\n\n" + nextChar;
  803. }
  804. );
  805. // attacklab: strip sentinel
  806. text = text.replace(/~0/, "");
  807. return text;
  808. }
  809. function hashBlock(text) {
  810. text = text.replace(/(^\n+|\n+$)/g, "");
  811. return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n";
  812. }
  813. function _DoCodeSpans(text) {
  814. //
  815. // * Backtick quotes are used for <code></code> spans.
  816. //
  817. // * You can use multiple backticks as the delimiters if you want to
  818. // include literal backticks in the code span. So, this input:
  819. //
  820. // Just type ``foo `bar` baz`` at the prompt.
  821. //
  822. // Will translate to:
  823. //
  824. // <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
  825. //
  826. // There's no arbitrary limit to the number of backticks you
  827. // can use as delimters. If you need three consecutive backticks
  828. // in your code, use four for delimiters, etc.
  829. //
  830. // * You can use spaces to get literal backticks at the edges:
  831. //
  832. // ... type `` `bar` `` ...
  833. //
  834. // Turns to:
  835. //
  836. // ... type <code>`bar`</code> ...
  837. //
  838. /*
  839. text = text.replace(/
  840. (^|[^\\]) // Character before opening ` can't be a backslash
  841. (`+) // $2 = Opening run of `
  842. ( // $3 = The code block
  843. [^\r]*?
  844. [^`] // attacklab: work around lack of lookbehind
  845. )
  846. \2 // Matching closer
  847. (?!`)
  848. /gm, function(){...});
  849. */
  850. text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
  851. function (wholeMatch, m1, m2, m3, m4) {
  852. var c = m3;
  853. c = c.replace(/^([ \t]*)/g, ""); // leading whitespace
  854. c = c.replace(/[ \t]*$/g, ""); // trailing whitespace
  855. c = _EncodeCode(c);
  856. 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.
  857. return m1 + "<code>" + c + "</code>";
  858. }
  859. );
  860. return text;
  861. }
  862. function _EncodeCode(text) {
  863. //
  864. // Encode/escape certain characters inside Markdown code runs.
  865. // The point is that in code, these characters are literals,
  866. // and lose their special Markdown meanings.
  867. //
  868. // Encode all ampersands; HTML entities are not
  869. // entities within a Markdown code span.
  870. text = text.replace(/&/g, "&amp;");
  871. // Do the angle bracket song and dance:
  872. text = text.replace(/</g, "&lt;");
  873. text = text.replace(/>/g, "&gt;");
  874. // Now, escape characters that are magic in Markdown:
  875. text = escapeCharacters(text, "\*_{}[]\\", false);
  876. // jj the line above breaks this:
  877. //---
  878. //* Item
  879. // 1. Subitem
  880. // special char: *
  881. //---
  882. return text;
  883. }
  884. function _DoItalicsAndBold(text) {
  885. // <strong> must go first:
  886. text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g,
  887. "$1<strong>$3</strong>$4");
  888. text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g,
  889. "$1<em>$3</em>$4");
  890. return text;
  891. }
  892. function _DoBlockQuotes(text) {
  893. /*
  894. text = text.replace(/
  895. ( // Wrap whole match in $1
  896. (
  897. ^[ \t]*>[ \t]? // '>' at the start of a line
  898. .+\n // rest of the first line
  899. (.+\n)* // subsequent consecutive lines
  900. \n* // blanks
  901. )+
  902. )
  903. /gm, function(){...});
  904. */
  905. text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
  906. function (wholeMatch, m1) {
  907. var bq = m1;
  908. // attacklab: hack around Konqueror 3.5.4 bug:
  909. // "----------bug".replace(/^-/g,"") == "bug"
  910. bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting
  911. // attacklab: clean up hack
  912. bq = bq.replace(/~0/g, "");
  913. bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines
  914. bq = _RunBlockGamut(bq); // recurse
  915. bq = bq.replace(/(^|\n)/g, "$1 ");
  916. // These leading spaces screw with <pre> content, so we need to fix that:
  917. bq = bq.replace(
  918. /(\s*<pre>[^\r]+?<\/pre>)/gm,
  919. function (wholeMatch, m1) {
  920. var pre = m1;
  921. // attacklab: hack around Konqueror 3.5.4 bug:
  922. pre = pre.replace(/^ /mg, "~0");
  923. pre = pre.replace(/~0/g, "");
  924. return pre;
  925. });
  926. return hashBlock("<blockquote>\n" + bq + "\n</blockquote>");
  927. }
  928. );
  929. return text;
  930. }
  931. function _FormParagraphs(text, doNotUnhash) {
  932. //
  933. // Params:
  934. // $text - string to process with html <p> tags
  935. //
  936. // Strip leading and trailing lines:
  937. text = text.replace(/^\n+/g, "");
  938. text = text.replace(/\n+$/g, "");
  939. var grafs = text.split(/\n{2,}/g);
  940. var grafsOut = [];
  941. var markerRe = /~K(\d+)K/;
  942. //
  943. // Wrap <p> tags.
  944. //
  945. var end = grafs.length;
  946. for (var i = 0; i < end; i++) {
  947. var str = grafs[i];
  948. // if this is an HTML marker, copy it
  949. if (markerRe.test(str)) {
  950. grafsOut.push(str);
  951. }
  952. else if (/\S/.test(str)) {
  953. str = _RunSpanGamut(str);
  954. str = str.replace(/^([ \t]*)/g, "<p>");
  955. str += "</p>"
  956. grafsOut.push(str);
  957. }
  958. }
  959. //
  960. // Unhashify HTML blocks
  961. //
  962. if (!doNotUnhash) {
  963. end = grafsOut.length;
  964. for (var i = 0; i < end; i++) {
  965. var foundAny = true;
  966. while (foundAny) { // we may need several runs, since the data may be nested
  967. foundAny = false;
  968. grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) {
  969. foundAny = true;
  970. return g_html_blocks[id];
  971. });
  972. }
  973. }
  974. }
  975. return grafsOut.join("\n\n");
  976. }
  977. function _EncodeAmpsAndAngles(text) {
  978. // Smart processing for ampersands and angle brackets that need to be encoded.
  979. // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
  980. // http://bumppo.net/projects/amputator/
  981. text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&amp;");
  982. // Encode naked <'s
  983. text = text.replace(/<(?![a-z\/?\$!])/gi, "&lt;");
  984. return text;
  985. }
  986. function _EncodeBackslashEscapes(text) {
  987. //
  988. // Parameter: String.
  989. // Returns: The string, with after processing the following backslash
  990. // escape sequences.
  991. //
  992. // attacklab: The polite way to do this is with the new
  993. // escapeCharacters() function:
  994. //
  995. // text = escapeCharacters(text,"\\",true);
  996. // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);
  997. //
  998. // ...but we're sidestepping its use of the (slow) RegExp constructor
  999. // as an optimization for Firefox. This function gets called a LOT.
  1000. text = text.replace(/\\(\\)/g, escapeCharacters_callback);
  1001. text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback);
  1002. return text;
  1003. }
  1004. function _DoAutoLinks(text) {
  1005. // note that at this point, all other URL in the text are already hyperlinked as <a href=""></a>
  1006. // *except* for the <http://www.foo.com> case
  1007. // automatically add < and > around unadorned raw hyperlinks
  1008. // must be preceded by space/BOF and followed by non-word/EOF character
  1009. text = text.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi, "$1<$2$3>$4");
  1010. // autolink anything like <http://example.com>
  1011. var replacer = function (wholematch, m1) { return "<a href=\"" + m1 + "\">" + pluginHooks.plainLinkText(m1) + "</a>"; }
  1012. text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer);
  1013. // Email addresses: <address@domain.foo>
  1014. /*
  1015. text = text.replace(/
  1016. <
  1017. (?:mailto:)?
  1018. (
  1019. [-.\w]+
  1020. \@
  1021. [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
  1022. )
  1023. >
  1024. /gi, _DoAutoLinks_callback());
  1025. */
  1026. var email_replacer = function(wholematch, m1) {
  1027. var mailto = 'mailto:'
  1028. var link
  1029. var email
  1030. if (m1.substring(0, mailto.length) != mailto){
  1031. link = mailto + m1;
  1032. email = m1;
  1033. } else {
  1034. link = m1;
  1035. email = m1.substring(mailto.length, m1.length);
  1036. }
  1037. return "<a href=\"" + link + "\">" + pluginHooks.plainLinkText(email) + "</a>";
  1038. }
  1039. text = text.replace(/<((?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+))>/gi, email_replacer);
  1040. return text;
  1041. }
  1042. function _UnescapeSpecialChars(text) {
  1043. //
  1044. // Swap back in all the special characters we've hidden.
  1045. //
  1046. text = text.replace(/~E(\d+)E/g,
  1047. function (wholeMatch, m1) {
  1048. var charCodeToReplace = parseInt(m1);
  1049. return String.fromCharCode(charCodeToReplace);
  1050. }
  1051. );
  1052. return text;
  1053. }
  1054. function _Outdent(text) {
  1055. //
  1056. // Remove one level of line-leading tabs or spaces
  1057. //
  1058. // attacklab: hack around Konqueror 3.5.4 bug:
  1059. // "----------bug".replace(/^-/g,"") == "bug"
  1060. text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width
  1061. // attacklab: clean up hack
  1062. text = text.replace(/~0/g, "")
  1063. return text;
  1064. }
  1065. function _Detab(text) {
  1066. if (!/\t/.test(text))
  1067. return text;
  1068. var spaces = [" ", " ", " ", " "],
  1069. skew = 0,
  1070. v;
  1071. return text.replace(/[\n\t]/g, function (match, offset) {
  1072. if (match === "\n") {
  1073. skew = offset + 1;
  1074. return match;
  1075. }
  1076. v = (offset - skew) % 4;
  1077. skew = offset + 1;
  1078. return spaces[v];
  1079. });
  1080. }
  1081. //
  1082. // attacklab: Utility functions
  1083. //
  1084. var _problemUrlChars = /(?:["'*()[\]:]|~D)/g;
  1085. // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems
  1086. function encodeProblemUrlChars(url) {
  1087. if (!url)
  1088. return "";
  1089. var len = url.length;
  1090. return url.replace(_problemUrlChars, function (match, offset) {
  1091. if (match == "~D") // escape for dollar
  1092. return "%24";
  1093. if (match == ":") {
  1094. if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1)))
  1095. return ":";
  1096. if (url.substring(0, 'mailto:'.length) === 'mailto:')
  1097. return ":";
  1098. if (url.substring(0, 'magnet:'.length) === 'magnet:')
  1099. return ":";
  1100. }
  1101. return "%" + match.charCodeAt(0).toString(16);
  1102. });
  1103. }
  1104. function escapeCharacters(text, charsToEscape, afterBackslash) {
  1105. // First we have to escape the escape characters so that
  1106. // we can build a character class out of them
  1107. var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])";
  1108. if (afterBackslash) {
  1109. regexString = "\\\\" + regexString;
  1110. }
  1111. var regex = new RegExp(regexString, "g");
  1112. text = text.replace(regex, escapeCharacters_callback);
  1113. return text;
  1114. }
  1115. function escapeCharacters_callback(wholeMatch, m1) {
  1116. var charCodeToEscape = m1.charCodeAt(0);
  1117. return "~E" + charCodeToEscape + "E";
  1118. }
  1119. }; // end of the Markdown.Converter constructor
  1120. })();
  1121. // needs Markdown.Converter.js at the moment
  1122. (function () {
  1123. var util = {},
  1124. position = {},
  1125. ui = {},
  1126. doc = window.document,
  1127. re = window.RegExp,
  1128. nav = window.navigator,
  1129. SETTINGS = { lineLength: 72 },
  1130. // Used to work around some browser bugs where we can't use feature testing.
  1131. uaSniffed = {
  1132. isIE: /msie/.test(nav.userAgent.toLowerCase()),
  1133. isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()),
  1134. isOpera: /opera/.test(nav.userAgent.toLowerCase())
  1135. };
  1136. // -------------------------------------------------------------------
  1137. // YOUR CHANGES GO HERE
  1138. //
  1139. // I've tried to localize the things you are likely to change to
  1140. // this area.
  1141. // -------------------------------------------------------------------
  1142. // The text that appears on the upper part of the dialog box when
  1143. // entering links.
  1144. var linkDialogText = "<code>http://example.com/ \"optional title\"</code>";
  1145. var imageDialogText = "<code>http://example.com/images/diagram.jpg \"optional title\"</code>";
  1146. // The default text that appears in the dialog input box when entering
  1147. // links.
  1148. var imageDefaultText = "http://";
  1149. var linkDefaultText = "http://";
  1150. var defaultHelpHoverTitle = "Markdown Editing Help";
  1151. // -------------------------------------------------------------------
  1152. // END OF YOUR CHANGES
  1153. // -------------------------------------------------------------------
  1154. // help, if given, should have a property "handler", the click handler for the help button,
  1155. // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help").
  1156. // If help isn't given, not help button is created.
  1157. //
  1158. // The constructed editor object has the methods:
  1159. // - getConverter() returns the markdown converter object that was passed to the constructor
  1160. // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
  1161. // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
  1162. Markdown.Editor = function (markdownConverter, idPostfix, help) {
  1163. idPostfix = idPostfix || "";
  1164. var hooks = this.hooks = new Markdown.HookCollection();
  1165. hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed
  1166. hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
  1167. hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates
  1168. * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
  1169. * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
  1170. */
  1171. this.getConverter = function () { return markdownConverter; }
  1172. var that = this,
  1173. panels;
  1174. this.run = function () {
  1175. if (panels)
  1176. return; // already initialized
  1177. panels = new PanelCollection(idPostfix);
  1178. var commandManager = new CommandManager(hooks);
  1179. var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });
  1180. var undoManager, uiManager;
  1181. if (!/\?noundo/.test(doc.location.href)) {
  1182. undoManager = new UndoManager(function () {
  1183. previewManager.refresh();
  1184. if (uiManager) // not available on the first call
  1185. uiManager.setUndoRedoButtonStates();
  1186. }, panels);
  1187. this.textOperation = function (f) {
  1188. undoManager.setCommandMode();
  1189. f();
  1190. that.refreshPreview();
  1191. }
  1192. }
  1193. uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help);
  1194. uiManager.setUndoRedoButtonStates();
  1195. var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
  1196. forceRefresh();
  1197. };
  1198. }
  1199. // before: contains all the text in the input box BEFORE the selection.
  1200. // after: contains all the text in the input box AFTER the selection.
  1201. function Chunks() { }
  1202. // startRegex: a regular expression to find the start tag
  1203. // endRegex: a regular expresssion to find the end tag
  1204. Chunks.prototype.findTags = function (startRegex, endRegex) {
  1205. var chunkObj = this;
  1206. var regex;
  1207. if (startRegex) {
  1208. regex = util.extendRegExp(startRegex, "", "$");
  1209. this.before = this.before.replace(regex,
  1210. function (match) {
  1211. chunkObj.startTag = chunkObj.startTag + match;
  1212. return "";
  1213. });
  1214. regex = util.extendRegExp(startRegex, "^", "");
  1215. this.selection = this.selection.replace(regex,
  1216. function (match) {
  1217. chunkObj.startTag = chunkObj.startTag + match;
  1218. return "";
  1219. });
  1220. }
  1221. if (endRegex) {
  1222. regex = util.extendRegExp(endRegex, "", "$");
  1223. this.selection = this.selection.replace(regex,
  1224. function (match) {
  1225. chunkObj.endTag = match + chunkObj.endTag;
  1226. return "";
  1227. });
  1228. regex = util.extendRegExp(endRegex, "^", "");
  1229. this.after = this.after.replace(regex,
  1230. function (match) {
  1231. chunkObj.endTag = match + chunkObj.endTag;
  1232. return "";
  1233. });
  1234. }
  1235. };
  1236. // If remove is false, the whitespace is transferred
  1237. // to the before/after regions.
  1238. //
  1239. // If remove is true, the whitespace disappears.
  1240. Chunks.prototype.trimWhitespace = function (remove) {
  1241. var beforeReplacer, afterReplacer, that = this;
  1242. if (remove) {
  1243. beforeReplacer = afterReplacer = "";
  1244. } else {
  1245. beforeReplacer = function (s) { that.before += s; return ""; }
  1246. afterReplacer = function (s) { that.after = s + that.after; return ""; }
  1247. }
  1248. this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
  1249. };
  1250. Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
  1251. if (nLinesBefore === undefined) {
  1252. nLinesBefore = 1;
  1253. }
  1254. if (nLinesAfter === undefined) {
  1255. nLinesAfter = 1;
  1256. }
  1257. nLinesBefore++;
  1258. nLinesAfter++;
  1259. var regexText;
  1260. var replacementText;
  1261. // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
  1262. if (navigator.userAgent.match(/Chrome/)) {
  1263. "X".match(/()./);
  1264. }
  1265. this.selection = this.selection.replace(/(^\n*)/, "");
  1266. this.startTag = this.startTag + re.$1;
  1267. this.selection = this.selection.replace(/(\n*$)/, "");
  1268. this.endTag = this.endTag + re.$1;
  1269. this.startTag = this.startTag.replace(/(^\n*)/, "");
  1270. this.before = this.before + re.$1;
  1271. this.endTag = this.endTag.replace(/(\n*$)/, "");
  1272. this.after = this.after + re.$1;
  1273. if (this.before) {
  1274. regexText = replacementText = "";
  1275. while (nLinesBefore--) {
  1276. regexText += "\\n?";
  1277. replacementText += "\n";
  1278. }
  1279. if (findExtraNewlines) {
  1280. regexText = "\\n*";
  1281. }
  1282. this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
  1283. }
  1284. if (this.after) {
  1285. regexText = replacementText = "";
  1286. while (nLinesAfter--) {
  1287. regexText += "\\n?";
  1288. replacementText += "\n";
  1289. }
  1290. if (findExtraNewlines) {
  1291. regexText = "\\n*";
  1292. }
  1293. this.after = this.after.replace(new re(regexText, ""), replacementText);
  1294. }
  1295. };
  1296. // end of Chunks
  1297. // A collection of the important regions on the page.
  1298. // Cached so we don't have to keep traversing the DOM.
  1299. // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
  1300. // this issue:
  1301. // Internet explorer has problems with CSS sprite buttons that use HTML
  1302. // lists. When you click on the background image "button", IE will
  1303. // select the non-existent link text and discard the selection in the
  1304. // textarea. The solution to this is to cache the textarea selection
  1305. // on the button's mousedown event and set a flag. In the part of the
  1306. // code where we need to grab the selection, we check for the flag
  1307. // and, if it's set, use the cached area instead of querying the
  1308. // textarea.
  1309. //
  1310. // This ONLY affects Internet Explorer (tested on versions 6, 7
  1311. // and 8) and ONLY on button clicks. Keyboard shortcuts work
  1312. // normally since the focus never leaves the textarea.
  1313. function PanelCollection(postfix) {
  1314. this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
  1315. this.preview = doc.getElementById("wmd-preview" + postfix);
  1316. this.input = doc.getElementById("wmd-input" + postfix);
  1317. };
  1318. // Returns true if the DOM element is visible, false if it's hidden.
  1319. // Checks if display is anything other than none.
  1320. util.isVisible = function (elem) {
  1321. if (window.getComputedStyle) {
  1322. // Most browsers
  1323. return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
  1324. }
  1325. else if (elem.currentStyle) {
  1326. // IE
  1327. return elem.currentStyle["display"] !== "none";
  1328. }
  1329. };
  1330. // Adds a listener callback to a DOM element which is fired on a specified
  1331. // event.
  1332. util.addEvent = function (elem, event, listener) {
  1333. if (elem.attachEvent) {
  1334. // IE only. The "on" is mandatory.
  1335. elem.attachEvent("on" + event, listener);
  1336. }
  1337. else {
  1338. // Other browsers.
  1339. elem.addEventListener(event, listener, false);
  1340. }
  1341. };
  1342. // Removes a listener callback from a DOM element which is fired on a specified
  1343. // event.
  1344. util.removeEvent = function (elem, event, listener) {
  1345. if (elem.detachEvent) {
  1346. // IE only. The "on" is mandatory.
  1347. elem.detachEvent("on" + event, listener);
  1348. }
  1349. else {
  1350. // Other browsers.
  1351. elem.removeEventListener(event, listener, false);
  1352. }
  1353. };
  1354. // Converts \r\n and \r to \n.
  1355. util.fixEolChars = function (text) {
  1356. text = text.replace(/\r\n/g, "\n");
  1357. text = text.replace(/\r/g, "\n");
  1358. return text;
  1359. };
  1360. // Extends a regular expression. Returns a new RegExp
  1361. // using pre + regex + post as the expression.
  1362. // Used in a few functions where we have a base
  1363. // expression and we want to pre- or append some
  1364. // conditions to it (e.g. adding "$" to the end).
  1365. // The flags are unchanged.
  1366. //
  1367. // regex is a RegExp, pre and post are strings.
  1368. util.extendRegExp = function (regex, pre, post) {
  1369. if (pre === null || pre === undefined) {
  1370. pre = "";
  1371. }
  1372. if (post === null || post === undefined) {
  1373. post = "";
  1374. }
  1375. var pattern = regex.toString();
  1376. var flags;
  1377. // Replace the flags with empty space and store them.
  1378. pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) {
  1379. flags = flagsPart;
  1380. return "";
  1381. });
  1382. // Remove the slash delimiters on the regular expression.
  1383. pattern = pattern.replace(/(^\/|\/$)/g, "");
  1384. pattern = pre + pattern + post;
  1385. return new re(pattern, flags);
  1386. }
  1387. // UNFINISHED
  1388. // The assignment in the while loop makes jslint cranky.
  1389. // I'll change it to a better loop later.
  1390. position.getTop = function (elem, isInner) {
  1391. var result = elem.offsetTop;
  1392. if (!isInner) {
  1393. while (elem = elem.offsetParent) {
  1394. result += elem.offsetTop;
  1395. }
  1396. }
  1397. return result;
  1398. };
  1399. position.getHeight = function (elem) {
  1400. return elem.offsetHeight || elem.scrollHeight;
  1401. };
  1402. position.getWidth = function (elem) {
  1403. return elem.offsetWidth || elem.scrollWidth;
  1404. };
  1405. position.getPageSize = function () {
  1406. var scrollWidth, scrollHeight;
  1407. var innerWidth, innerHeight;
  1408. // It's not very clear which blocks work with which browsers.
  1409. if (self.innerHeight && self.scrollMaxY) {
  1410. scrollWidth = doc.body.scrollWidth;
  1411. scrollHeight = self.innerHeight + self.scrollMaxY;
  1412. }
  1413. else if (doc.body.scrollHeight > doc.body.offsetHeight) {
  1414. scrollWidth = doc.body.scrollWidth;
  1415. scrollHeight = doc.body.scrollHeight;
  1416. }
  1417. else {
  1418. scrollWidth = doc.body.offsetWidth;
  1419. scrollHeight = doc.body.offsetHeight;
  1420. }
  1421. if (self.innerHeight) {
  1422. // Non-IE browser
  1423. innerWidth = self.innerWidth;
  1424. innerHeight = self.innerHeight;
  1425. }
  1426. else if (doc.documentElement && doc.documentElement.clientHeight) {
  1427. // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
  1428. innerWidth = doc.documentElement.clientWidth;
  1429. innerHeight = doc.documentElement.clientHeight;
  1430. }
  1431. else if (doc.body) {
  1432. // Other versions of IE
  1433. innerWidth = doc.body.clientWidth;
  1434. innerHeight = doc.body.clientHeight;
  1435. }
  1436. var maxWidth = Math.max(scrollWidth, innerWidth);
  1437. var maxHeight = Math.max(scrollHeight, innerHeight);
  1438. return [maxWidth, maxHeight, innerWidth, innerHeight];
  1439. };
  1440. // Handles pushing and popping TextareaStates for undo/redo commands.
  1441. // I should rename the stack variables to list.
  1442. function UndoManager(callback, panels) {
  1443. var undoObj = this;
  1444. var undoStack = []; // A stack of undo states
  1445. var stackPtr = 0; // The index of the current state
  1446. var mode = "none";
  1447. var lastState; // The last state
  1448. var timer; // The setTimeout handle for cancelling the timer
  1449. var inputStateObj;
  1450. // Set the mode for later logic steps.
  1451. var setMode = function (newMode, noSave) {
  1452. if (mode != newMode) {
  1453. mode = newMode;
  1454. if (!noSave) {
  1455. saveState();
  1456. }
  1457. }
  1458. if (!uaSniffed.isIE || mode != "moving") {
  1459. timer = setTimeout(refreshState, 1);
  1460. }
  1461. else {
  1462. inputStateObj = null;
  1463. }
  1464. };
  1465. var refreshState = function (isInitialState) {
  1466. inputStateObj = new TextareaState(panels, isInitialState);
  1467. timer = undefined;
  1468. };
  1469. this.setCommandMode = function () {
  1470. mode = "command";
  1471. saveState();
  1472. timer = setTimeout(refreshState, 0);
  1473. };
  1474. this.canUndo = function () {
  1475. return stackPtr > 1;
  1476. };
  1477. this.canRedo = function () {
  1478. if (undoStack[stackPtr + 1]) {
  1479. return true;
  1480. }
  1481. return false;
  1482. };
  1483. // Removes the last state and restores it.
  1484. this.undo = function () {
  1485. if (undoObj.canUndo()) {
  1486. if (lastState) {
  1487. // What about setting state -1 to null or checking for undefined?
  1488. lastState.restore();
  1489. lastState = null;
  1490. }
  1491. else {
  1492. undoStack[stackPtr] = new TextareaState(panels);
  1493. undoStack[--stackPtr].restore();
  1494. if (callback) {
  1495. callback();
  1496. }
  1497. }
  1498. }
  1499. mode = "none";
  1500. panels.input.focus();
  1501. refreshState();
  1502. };
  1503. // Redo an action.
  1504. this.redo = function () {
  1505. if (undoObj.canRedo()) {
  1506. undoStack[++stackPtr].restore();
  1507. if (callback) {
  1508. callback();
  1509. }
  1510. }
  1511. mode = "none";
  1512. panels.input.focus();
  1513. refreshState();
  1514. };
  1515. // Push the input area state to the stack.
  1516. var saveState = function () {
  1517. var currState = inputStateObj || new TextareaState(panels);
  1518. if (!currState) {
  1519. return false;
  1520. }
  1521. if (mode == "moving") {
  1522. if (!lastState) {
  1523. lastState = currState;
  1524. }
  1525. return;
  1526. }
  1527. if (lastState) {
  1528. if (undoStack[stackPtr - 1].text != lastState.text) {
  1529. undoStack[stackPtr++] = lastState;
  1530. }
  1531. lastState = null;
  1532. }
  1533. undoStack[stackPtr++] = currState;
  1534. undoStack[stackPtr + 1] = null;
  1535. if (callback) {
  1536. callback();
  1537. }
  1538. };
  1539. var handleCtrlYZ = function (event) {
  1540. var handled = false;
  1541. if (event.ctrlKey || event.metaKey) {
  1542. // IE and Opera do not support charCode.
  1543. var keyCode = event.charCode || event.keyCode;
  1544. var keyCodeChar = String.fromCharCode(keyCode);
  1545. switch (keyCodeChar) {
  1546. case "y":
  1547. undoObj.redo();
  1548. handled = true;
  1549. break;
  1550. case "z":
  1551. if (!event.shiftKey) {
  1552. undoObj.undo();
  1553. }
  1554. else {
  1555. undoObj.redo();
  1556. }
  1557. handled = true;
  1558. break;
  1559. }
  1560. }
  1561. if (handled) {
  1562. if (event.preventDefault) {
  1563. event.preventDefault();
  1564. }
  1565. if (window.event) {
  1566. window.event.returnValue = false;
  1567. }
  1568. return;
  1569. }
  1570. };
  1571. // Set the mode depending on what is going on in the input area.
  1572. var handleModeChange = function (event) {
  1573. if (!event.ctrlKey && !event.metaKey) {
  1574. var keyCode = event.keyCode;
  1575. if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
  1576. // 33 - 40: page up/dn and arrow keys
  1577. // 63232 - 63235: page up/dn and arrow keys on safari
  1578. setMode("moving");
  1579. }
  1580. else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
  1581. // 8: backspace
  1582. // 46: delete
  1583. // 127: delete
  1584. setMode("deleting");
  1585. }
  1586. else if (keyCode == 13) {
  1587. // 13: Enter
  1588. setMode("newlines");
  1589. }
  1590. else if (keyCode == 27) {
  1591. // 27: escape
  1592. setMode("escape");
  1593. }
  1594. else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
  1595. // 16-20 are shift, etc.
  1596. // 91: left window key
  1597. // I think this might be a little messed up since there are
  1598. // a lot of nonprinting keys above 20.
  1599. setMode("typing");
  1600. }
  1601. }
  1602. };
  1603. var setEventHandlers = function () {
  1604. util.addEvent(panels.input, "keypress", function (event) {
  1605. // keyCode 89: y
  1606. // keyCode 90: z
  1607. if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
  1608. event.preventDefault();
  1609. }
  1610. });
  1611. var handlePaste = function () {
  1612. if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {
  1613. if (timer == undefined) {
  1614. mode = "paste";
  1615. saveState();
  1616. refreshState();
  1617. }
  1618. }
  1619. };
  1620. util.addEvent(panels.input, "keydown", handleCtrlYZ);
  1621. util.addEvent(panels.input, "keydown", handleModeChange);
  1622. util.addEvent(panels.input, "mousedown", function () {
  1623. setMode("moving");
  1624. });
  1625. panels.input.onpaste = handlePaste;
  1626. panels.input.ondrop = handlePaste;
  1627. };
  1628. var init = function () {
  1629. setEventHandlers();
  1630. refreshState(true);
  1631. saveState();
  1632. };
  1633. init();
  1634. }
  1635. // end of UndoManager
  1636. // The input textarea state/contents.
  1637. // This is used to implement undo/redo by the undo manager.
  1638. function TextareaState(panels, isInitialState) {
  1639. // Aliases
  1640. var stateObj = this;
  1641. var inputArea = panels.input;
  1642. this.init = function () {
  1643. if (!util.isVisible(inputArea)) {
  1644. return;
  1645. }
  1646. if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box
  1647. return;
  1648. }
  1649. this.setInputAreaSelectionStartEnd();
  1650. this.scrollTop = inputArea.scrollTop;
  1651. if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
  1652. this.text = inputArea.value;
  1653. }
  1654. }
  1655. // Sets the selected text in the input box after we've performed an
  1656. // operation.
  1657. this.setInputAreaSelection = function () {
  1658. if (!util.isVisible(inputArea)) {
  1659. return;
  1660. }
  1661. if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {
  1662. inputArea.focus();
  1663. inputArea.selectionStart = stateObj.start;
  1664. inputArea.selectionEnd = stateObj.end;
  1665. inputArea.scrollTop = stateObj.scrollTop;
  1666. }
  1667. else if (doc.selection) {
  1668. if (doc.activeElement && doc.activeElement !== inputArea) {
  1669. return;
  1670. }
  1671. inputArea.focus();
  1672. var range = inputArea.createTextRange();
  1673. range.moveStart("character", -inputArea.value.length);
  1674. range.moveEnd("character", -inputArea.value.length);
  1675. range.moveEnd("character", stateObj.end);
  1676. range.moveStart("character", stateObj.start);
  1677. range.select();
  1678. }
  1679. };
  1680. this.setInputAreaSelectionStartEnd = function () {
  1681. if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) {
  1682. stateObj.start = inputArea.selectionStart;
  1683. stateObj.end = inputArea.selectionEnd;
  1684. }
  1685. else if (doc.selection) {
  1686. stateObj.text = util.fixEolChars(inputArea.value);
  1687. // IE loses the selection in the textarea when buttons are
  1688. // clicked. On IE we cache the selection. Here, if something is cached,
  1689. // we take it.
  1690. var range = panels.ieCachedRange || doc.selection.createRange();
  1691. var fixedRange = util.fixEolChars(range.text);
  1692. var marker = "\x07";
  1693. var markedRange = marker + fixedRange + marker;
  1694. range.text = markedRange;
  1695. var inputText = util.fixEolChars(inputArea.value);
  1696. range.moveStart("character", -markedRange.length);
  1697. range.text = fixedRange;
  1698. stateObj.start = inputText.indexOf(marker);
  1699. stateObj.end = inputText.lastIndexOf(marker) - marker.length;
  1700. var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
  1701. if (len) {
  1702. range.moveStart("character", -fixedRange.length);
  1703. while (len--) {
  1704. fixedRange += "\n";
  1705. stateObj.end += 1;
  1706. }
  1707. range.text = fixedRange;
  1708. }
  1709. if (panels.ieCachedRange)
  1710. stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange
  1711. panels.ieCachedRange = null;
  1712. this.setInputAreaSelection();
  1713. }
  1714. };
  1715. // Restore this state into the input area.
  1716. this.restore = function () {
  1717. if (stateObj.text != undefined && stateObj.text != inputArea.value) {
  1718. inputArea.value = stateObj.text;
  1719. }
  1720. this.setInputAreaSelection();
  1721. inputArea.scrollTop = stateObj.scrollTop;
  1722. };
  1723. // Gets a collection of HTML chunks from the inptut textarea.
  1724. this.getChunks = function () {
  1725. var chunk = new Chunks();
  1726. chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
  1727. chunk.startTag = "";
  1728. chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
  1729. chunk.endTag = "";
  1730. chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
  1731. chunk.scrollTop = stateObj.scrollTop;
  1732. return chunk;
  1733. };
  1734. // Sets the TextareaState properties given a chunk of markdown.
  1735. this.setChunks = function (chunk) {
  1736. chunk.before = chunk.before + chunk.startTag;
  1737. chunk.after = chunk.endTag + chunk.after;
  1738. this.start = chunk.before.length;
  1739. this.end = chunk.before.length + chunk.selection.length;
  1740. this.text = chunk.before + chunk.selection + chunk.after;
  1741. this.scrollTop = chunk.scrollTop;
  1742. };
  1743. this.init();
  1744. };
  1745. function PreviewManager(converter, panels, previewRefreshCallback) {
  1746. var managerObj = this;
  1747. var timeout;
  1748. var elapsedTime;
  1749. var oldInputText;
  1750. var maxDelay = 3000;
  1751. var startType = "delayed"; // The other legal value is "manual"
  1752. // Adds event listeners to elements
  1753. var setupEvents = function (inputElem, listener) {
  1754. util.addEvent(inputElem, "input", listener);
  1755. inputElem.onpaste = listener;
  1756. inputElem.ondrop = listener;
  1757. util.addEvent(inputElem, "keypress", listener);
  1758. util.addEvent(inputElem, "keydown", listener);
  1759. };
  1760. var getDocScrollTop = function () {
  1761. var result = 0;
  1762. if (window.innerHeight) {
  1763. result = window.pageYOffset;
  1764. }
  1765. else
  1766. if (doc.documentElement && doc.documentElement.scrollTop) {
  1767. result = doc.documentElement.scrollTop;
  1768. }
  1769. else
  1770. if (doc.body) {
  1771. result = doc.body.scrollTop;
  1772. }
  1773. return result;
  1774. };
  1775. var makePreviewHtml = function () {
  1776. // If there is no registered preview panel
  1777. // there is nothing to do.
  1778. if (!panels.preview)
  1779. return;
  1780. var text = panels.input.value;
  1781. if (text && text == oldInputText) {
  1782. return; // Input text hasn't changed.
  1783. }
  1784. else {
  1785. oldInputText = text;
  1786. }
  1787. var prevTime = new Date().getTime();
  1788. text = converter.makeHtml(text);
  1789. // Calculate the processing time of the HTML creation.
  1790. // It's used as the delay time in the event listener.
  1791. var currTime = new Date().getTime();
  1792. elapsedTime = currTime - prevTime;
  1793. pushPreviewHtml(text);
  1794. };
  1795. // setTimeout is already used. Used as an event listener.
  1796. var applyTimeout = function () {
  1797. if (timeout) {
  1798. clearTimeout(timeout);
  1799. timeout = undefined;
  1800. }
  1801. if (startType !== "manual") {
  1802. var delay = 0;
  1803. if (startType === "delayed") {
  1804. delay = elapsedTime;
  1805. }
  1806. if (delay > maxDelay) {
  1807. delay = maxDelay;
  1808. }
  1809. timeout = setTimeout(makePreviewHtml, delay);
  1810. }
  1811. };
  1812. var getScaleFactor = function (panel) {
  1813. if (panel.scrollHeight <= panel.clientHeight) {
  1814. return 1;
  1815. }
  1816. return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
  1817. };
  1818. var setPanelScrollTops = function () {
  1819. if (panels.preview) {
  1820. panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
  1821. }
  1822. };
  1823. this.refresh = function (requiresRefresh) {
  1824. if (requiresRefresh) {
  1825. oldInputText = "";
  1826. makePreviewHtml();
  1827. }
  1828. else {
  1829. applyTimeout();
  1830. }
  1831. };
  1832. this.processingTime = function () {
  1833. return elapsedTime;
  1834. };
  1835. var isFirstTimeFilled = true;
  1836. // IE doesn't let you use innerHTML if the element is contained somewhere in a table
  1837. // (which is the case for inline editing) -- in that case, detach the element, set the
  1838. // value, and reattach. Yes, that *is* ridiculous.
  1839. var ieSafePreviewSet = function (text) {
  1840. var preview = panels.preview;
  1841. var parent = preview.parentNode;
  1842. var sibling = preview.nextSibling;
  1843. parent.removeChild(preview);
  1844. preview.innerHTML = text;
  1845. if (!sibling)
  1846. parent.appendChild(preview);
  1847. else
  1848. parent.insertBefore(preview, sibling);
  1849. }
  1850. var nonSuckyBrowserPreviewSet = function (text) {
  1851. panels.preview.innerHTML = text;
  1852. }
  1853. var previewSetter;
  1854. var previewSet = function (text) {
  1855. if (previewSetter)
  1856. return previewSetter(text);
  1857. try {
  1858. nonSuckyBrowserPreviewSet(text);
  1859. previewSetter = nonSuckyBrowserPreviewSet;
  1860. } catch (e) {
  1861. previewSetter = ieSafePreviewSet;
  1862. previewSetter(text);
  1863. }
  1864. };
  1865. var pushPreviewHtml = function (text) {
  1866. var emptyTop = position.getTop(panels.input) - getDocScrollTop();
  1867. if (panels.preview) {
  1868. previewSet(text);
  1869. previewRefreshCallback();
  1870. }
  1871. setPanelScrollTops();
  1872. if (isFirstTimeFilled) {
  1873. isFirstTimeFilled = false;
  1874. return;
  1875. }
  1876. var fullTop = position.getTop(panels.input) - getDocScrollTop();
  1877. if (uaSniffed.isIE) {
  1878. setTimeout(function () {
  1879. window.scrollBy(0, fullTop - emptyTop);
  1880. }, 0);
  1881. }
  1882. else {
  1883. window.scrollBy(0, fullTop - emptyTop);
  1884. }
  1885. };
  1886. var init = function () {
  1887. setupEvents(panels.input, applyTimeout);
  1888. makePreviewHtml();
  1889. if (panels.preview) {
  1890. panels.preview.scrollTop = 0;
  1891. }
  1892. };
  1893. init();
  1894. };
  1895. // This simulates a modal dialog box and asks for the URL when you
  1896. // click the hyperlink or image buttons.
  1897. //
  1898. // text: The html for the input box.
  1899. // defaultInputText: The default value that appears in the input box.
  1900. // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
  1901. // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
  1902. // was chosen).
  1903. ui.prompt = function (title, text, defaultInputText, callback) {
  1904. // These variables need to be declared at this level since they are used
  1905. // in multiple functions.
  1906. var dialog; // The dialog box.
  1907. var input; // The text box where you enter the hyperlink.
  1908. if (defaultInputText === undefined) {
  1909. defaultInputText = "";
  1910. }
  1911. // Used as a keydown event handler. Esc dismisses the prompt.
  1912. // Key code 27 is ESC.
  1913. var checkEscape = function (key) {
  1914. var code = (key.charCode || key.keyCode);
  1915. if (code === 27) {
  1916. close(true);
  1917. }
  1918. };
  1919. // Dismisses the hyperlink input box.
  1920. // isCancel is true if we don't care about the input text.
  1921. // isCancel is false if we are going to keep the text.
  1922. var close = function (isCancel) {
  1923. util.removeEvent(doc.body, "keydown", checkEscape);
  1924. var text = input.value;
  1925. if (isCancel) {
  1926. text = null;
  1927. }
  1928. else {
  1929. // Fixes common pasting errors.
  1930. text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
  1931. if (!/^(?:https?|ftp):\/\//.test(text))
  1932. text = 'http://' + text;
  1933. }
  1934. $(dialog).modal('hide');
  1935. callback(text);
  1936. return false;
  1937. };
  1938. // Create the text input box form/window.
  1939. var createDialog = function () {
  1940. // <div class="modal" id="myModal">
  1941. // <div class="modal-header">
  1942. // <a class="close" data-dismiss="modal">×</a>
  1943. // <h3>Modal header</h3>
  1944. // </div>
  1945. // <div class="modal-body">
  1946. // <p>One fine body…</p>
  1947. // </div>
  1948. // <div class="modal-footer">
  1949. // <a href="#" class="btn btn-primary">Save changes</a>
  1950. // <a href="#" class="btn">Close</a>
  1951. // </div>
  1952. // </div>
  1953. // The main dialog box.
  1954. dialog = doc.createElement("div");
  1955. dialog.className = "modal fade";
  1956. // The modal-dialog div.
  1957. var div_dialog = doc.createElement("div");
  1958. div_dialog.className = "modal-dialog";
  1959. dialog.appendChild(div_dialog);
  1960. // The modal-content div.
  1961. var content = doc.createElement("div");
  1962. content.className = "modal-content";
  1963. div_dialog.appendChild(content);
  1964. // The header.
  1965. var header = doc.createElement("div");
  1966. header.className = "modal-header";
  1967. header.innerHTML = '<a class="close" data-dismiss="modal">×</a> <h3 class="modal-title">'+title+'</h3>';
  1968. content.appendChild(header);
  1969. // The body.
  1970. var body = doc.createElement("div");
  1971. body.className = "modal-body";
  1972. content.appendChild(body);
  1973. // The footer.
  1974. var footer = doc.createElement("div");
  1975. footer.className = "modal-footer";
  1976. content.appendChild(footer);
  1977. // The dialog text.
  1978. var question = doc.createElement("p");
  1979. question.innerHTML = text;
  1980. question.style.padding = "5px";
  1981. body.appendChild(question);
  1982. // The web form container for the text box and buttons.
  1983. var form = doc.createElement("form"),
  1984. style = form.style;
  1985. form.onsubmit = function () { return close(false); };
  1986. style.padding = "0";
  1987. style.margin = "0";
  1988. body.appendChild(form);
  1989. // The input text box
  1990. input = doc.createElement("input");
  1991. input.type = "text";
  1992. input.value = defaultInputText;
  1993. input.className = "form-control";
  1994. style = input.style;
  1995. style.display = "block";
  1996. style.width = "80%";
  1997. style.marginLeft = style.marginRight = "auto";
  1998. form.appendChild(input);
  1999. // The ok button
  2000. var okButton = doc.createElement("button");
  2001. okButton.className = "btn btn-primary";
  2002. okButton.type = "button";
  2003. okButton.onclick = function () { return close(false); };
  2004. okButton.innerHTML = "OK";
  2005. // The cancel button
  2006. var cancelButton = doc.createElement("button");
  2007. cancelButton.className = "btn btn-danger";
  2008. cancelButton.type = "button";
  2009. cancelButton.onclick = function () { return close(true); };
  2010. cancelButton.innerHTML = "Cancel";
  2011. footer.appendChild(okButton);
  2012. footer.appendChild(cancelButton);
  2013. util.addEvent(doc.body, "keydown", checkEscape);
  2014. doc.body.appendChild(dialog);
  2015. };
  2016. // Why is this in a zero-length timeout?
  2017. // Is it working around a browser bug?
  2018. setTimeout(function () {
  2019. createDialog();
  2020. var defTextLen = defaultInputText.length;
  2021. if (input.selectionStart !== undefined) {
  2022. input.selectionStart = 0;
  2023. input.selectionEnd = defTextLen;
  2024. }
  2025. else if (input.createTextRange) {
  2026. var range = input.createTextRange();
  2027. range.collapse(false);
  2028. range.moveStart("character", -defTextLen);
  2029. range.moveEnd("character", defTextLen);
  2030. range.select();
  2031. }
  2032. $(dialog).on('shown', function () {
  2033. input.focus();
  2034. })
  2035. $(dialog).on('hidden', function () {
  2036. dialog.parentNode.removeChild(dialog);
  2037. })
  2038. $(dialog).modal()
  2039. }, 0);
  2040. };
  2041. function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) {
  2042. var inputBox = panels.input,
  2043. buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
  2044. makeSpritedButtonRow();
  2045. var keyEvent = "keydown";
  2046. if (uaSniffed.isOpera) {
  2047. keyEvent = "keypress";
  2048. }
  2049. util.addEvent(inputBox, keyEvent, function (key) {
  2050. // Check to see if we have a button key and, if so execute the callback.
  2051. if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
  2052. var keyCode = key.charCode || key.keyCode;
  2053. var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
  2054. switch (keyCodeStr) {
  2055. case "b":
  2056. doClick(buttons.bold);
  2057. break;
  2058. case "i":
  2059. doClick(buttons.italic);
  2060. break;
  2061. case "l":
  2062. doClick(buttons.link);
  2063. break;
  2064. case "q":
  2065. doClick(buttons.quote);
  2066. break;
  2067. case "k":
  2068. doClick(buttons.code);
  2069. break;
  2070. case "g":
  2071. doClick(buttons.image);
  2072. break;
  2073. case "o":
  2074. doClick(buttons.olist);
  2075. break;
  2076. case "u":
  2077. doClick(buttons.ulist);
  2078. break;
  2079. case "h":
  2080. doClick(buttons.heading);
  2081. break;
  2082. case "r":
  2083. doClick(buttons.hr);
  2084. break;
  2085. case "y":
  2086. doClick(buttons.redo);
  2087. break;
  2088. case "z":
  2089. if (key.shiftKey) {
  2090. doClick(buttons.redo);
  2091. }
  2092. else {
  2093. doClick(buttons.undo);
  2094. }
  2095. break;
  2096. default:
  2097. return;
  2098. }
  2099. if (key.preventDefault) {
  2100. key.preventDefault();
  2101. }
  2102. if (window.event) {
  2103. window.event.returnValue = false;
  2104. }
  2105. }
  2106. });
  2107. // Auto-indent on shift-enter
  2108. util.addEvent(inputBox, "keyup", function (key) {
  2109. if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
  2110. var keyCode = key.charCode || key.keyCode;
  2111. // Character 13 is Enter
  2112. if (keyCode === 13) {
  2113. var fakeButton = {};
  2114. fakeButton.textOp = bindCommand("doAutoindent");
  2115. doClick(fakeButton);
  2116. }
  2117. }
  2118. });
  2119. // special handler because IE clears the context of the textbox on ESC
  2120. if (uaSniffed.isIE) {
  2121. util.addEvent(inputBox, "keydown", function (key) {
  2122. var code = key.keyCode;
  2123. if (code === 27) {
  2124. return false;
  2125. }
  2126. });
  2127. }
  2128. // Perform the button's action.
  2129. function doClick(button) {
  2130. inputBox.focus();
  2131. if (button.textOp) {
  2132. if (undoManager) {
  2133. undoManager.setCommandMode();
  2134. }
  2135. var state = new TextareaState(panels);
  2136. if (!state) {
  2137. return;
  2138. }
  2139. var chunks = state.getChunks();
  2140. // Some commands launch a "modal" prompt dialog. Javascript
  2141. // can't really make a modal dialog box and the WMD code
  2142. // will continue to execute while the dialog is displayed.
  2143. // This prevents the dialog pattern I'm used to and means
  2144. // I can't do something like this:
  2145. //
  2146. // var link = CreateLinkDialog();
  2147. // makeMarkdownLink(link);
  2148. //
  2149. // Instead of this straightforward method of handling a
  2150. // dialog I have to pass any code which would execute
  2151. // after the dialog is dismissed (e.g. link creation)
  2152. // in a function parameter.
  2153. //
  2154. // Yes this is awkward and I think it sucks, but there's
  2155. // no real workaround. Only the image and link code
  2156. // create dialogs and require the function pointers.
  2157. var fixupInputArea = function () {
  2158. inputBox.focus();
  2159. if (chunks) {
  2160. state.setChunks(chunks);
  2161. }
  2162. state.restore();
  2163. previewManager.refresh();
  2164. };
  2165. var noCleanup = button.textOp(chunks, fixupInputArea);
  2166. if (!noCleanup) {
  2167. fixupInputArea();
  2168. }
  2169. }
  2170. if (button.execute) {
  2171. button.execute(undoManager);
  2172. }
  2173. };
  2174. function setupButton(button, isEnabled) {
  2175. if (isEnabled) {
  2176. button.disabled = false;
  2177. if (!button.isHelp) {
  2178. button.onclick = function () {
  2179. if (this.onmouseout) {
  2180. this.onmouseout();
  2181. }
  2182. doClick(this);
  2183. return false;
  2184. }
  2185. }
  2186. }
  2187. else {
  2188. button.disabled = true;
  2189. }
  2190. }
  2191. function bindCommand(method) {
  2192. if (typeof method === "string")
  2193. method = commandManager[method];
  2194. return function () { method.apply(commandManager, arguments); }
  2195. }
  2196. function makeSpritedButtonRow() {
  2197. var buttonBar = panels.buttonBar;
  2198. var buttonRow = document.createElement("div");
  2199. buttonRow.id = "wmd-button-row" + postfix;
  2200. buttonRow.className = 'btn-toolbar';
  2201. buttonRow = buttonBar.appendChild(buttonRow);
  2202. var makeButton = function (id, title, icon, textOp, group) {
  2203. var button = document.createElement("button");
  2204. button.className = "btn btn-default";
  2205. var buttonImage = document.createElement("i");
  2206. buttonImage.className = icon;
  2207. button.id = id + postfix;
  2208. button.appendChild(buttonImage);
  2209. button.title = title;
  2210. $(button).tooltip({placement: 'bottom', container: 'body'})
  2211. if (textOp)
  2212. button.textOp = textOp;
  2213. setupButton(button, true);
  2214. if (group) {
  2215. group.appendChild(button);
  2216. } else {
  2217. buttonRow.appendChild(button);
  2218. }
  2219. return button;
  2220. };
  2221. var makeGroup = function (num) {
  2222. var group = document.createElement("div");
  2223. group.className = "btn-group wmd-button-group" + num;
  2224. group.id = "wmd-button-group" + num + postfix;
  2225. buttonRow.appendChild(group);
  2226. return group
  2227. }
  2228. group1 = makeGroup(1);
  2229. buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "glyph-icon icon-bold", bindCommand("doBold"), group1);
  2230. buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "glyph-icon icon-italic", bindCommand("doItalic"), group1);
  2231. group2 = makeGroup(2);
  2232. buttons.link = makeButton("wmd-link-button", "Link - Ctrl+L", "glyph-icon icon-link", bindCommand(function (chunk, postProcessing) {
  2233. return this.doLinkOrImage(chunk, postProcessing, false);
  2234. }), group2);
  2235. buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "glyph-icon icon-quote-left", bindCommand("doBlockquote"), group2);
  2236. buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "glyph-icon icon-code", bindCommand("doCode"), group2);
  2237. buttons.image = makeButton("wmd-image-button", "Image - Ctrl+G", "glyph-icon icon-picture-o", bindCommand(function (chunk, postProcessing) {
  2238. return this.doLinkOrImage(chunk, postProcessing, true);
  2239. }), group2);
  2240. group3 = makeGroup(3);
  2241. buttons.olist = makeButton("wmd-olist-button", "Numbered List - Ctrl+O", "glyph-icon icon-list-ol", bindCommand(function (chunk, postProcessing) {
  2242. this.doList(chunk, postProcessing, true);
  2243. }), group3);
  2244. buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List - Ctrl+U", "glyph-icon icon-list-ul", bindCommand(function (chunk, postProcessing) {
  2245. this.doList(chunk, postProcessing, false);
  2246. }), group3);
  2247. buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "glyph-icon icon-header", bindCommand("doHeading"), group3);
  2248. buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "glyph-icon icon-ellipsis-h", bindCommand("doHorizontalRule"), group3);
  2249. group4 = makeGroup(4);
  2250. buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "glyph-icon icon-undo", null, group4);
  2251. buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
  2252. var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
  2253. "Redo - Ctrl+Y" :
  2254. "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms
  2255. buttons.redo = makeButton("wmd-redo-button", redoTitle, "glyph-icon icon-rotate-right", null, group4);
  2256. buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
  2257. if (helpOptions) {
  2258. group5 = makeGroup(5);
  2259. group5.className = group5.className + " pull-right";
  2260. var helpButton = document.createElement("button");
  2261. var helpButtonImage = document.createElement("i");
  2262. helpButtonImage.className = "glyph-icon icon-question";
  2263. helpButton.appendChild(helpButtonImage);
  2264. helpButton.className = "btn";
  2265. helpButton.id = "wmd-help-button" + postfix;
  2266. helpButton.isHelp = true;
  2267. helpButton.title = helpOptions.title || defaultHelpHoverTitle;
  2268. $(helpButton).tooltip({placement: 'bottom', container: 'body'})
  2269. helpButton.onclick = helpOptions.handler;
  2270. setupButton(helpButton, true);
  2271. group5.appendChild(helpButton);
  2272. buttons.help = helpButton;
  2273. }
  2274. setUndoRedoButtonStates();
  2275. }
  2276. function setUndoRedoButtonStates() {
  2277. if (undoManager) {
  2278. setupButton(buttons.undo, undoManager.canUndo());
  2279. setupButton(buttons.redo, undoManager.canRedo());
  2280. }
  2281. };
  2282. this.setUndoRedoButtonStates = setUndoRedoButtonStates;
  2283. }
  2284. function CommandManager(pluginHooks) {
  2285. this.hooks = pluginHooks;
  2286. }
  2287. var commandProto = CommandManager.prototype;
  2288. // The markdown symbols - 4 spaces = code, > = blockquote, etc.
  2289. commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
  2290. // Remove markdown symbols from the chunk selection.
  2291. commandProto.unwrap = function (chunk) {
  2292. var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
  2293. chunk.selection = chunk.selection.replace(txt, "$1 $2");
  2294. };
  2295. commandProto.wrap = function (chunk, len) {
  2296. this.unwrap(chunk);
  2297. var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"),
  2298. that = this;
  2299. chunk.selection = chunk.selection.replace(regex, function (line, marked) {
  2300. if (new re("^" + that.prefixes, "").test(line)) {
  2301. return line;
  2302. }
  2303. return marked + "\n";
  2304. });
  2305. chunk.selection = chunk.selection.replace(/\s+$/, "");
  2306. };
  2307. commandProto.doBold = function (chunk, postProcessing) {
  2308. return this.doBorI(chunk, postProcessing, 2, "strong text");
  2309. };
  2310. commandProto.doItalic = function (chunk, postProcessing) {
  2311. return this.doBorI(chunk, postProcessing, 1, "emphasized text");
  2312. };
  2313. // chunk: The selected region that will be enclosed with */**
  2314. // nStars: 1 for italics, 2 for bold
  2315. // insertText: If you just click the button without highlighting text, this gets inserted
  2316. commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
  2317. // Get rid of whitespace and fixup newlines.
  2318. chunk.trimWhitespace();
  2319. chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
  2320. // Look for stars before and after. Is the chunk already marked up?
  2321. // note that these regex matches cannot fail
  2322. var starsBefore = /(\**$)/.exec(chunk.before)[0];
  2323. var starsAfter = /(^\**)/.exec(chunk.after)[0];
  2324. var prevStars = Math.min(starsBefore.length, starsAfter.length);
  2325. // Remove stars if we have to since the button acts as a toggle.
  2326. if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
  2327. chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
  2328. chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
  2329. }
  2330. else if (!chunk.selection && starsAfter) {
  2331. // It's not really clear why this code is necessary. It just moves
  2332. // some arbitrary stuff around.
  2333. chunk.after = chunk.after.replace(/^([*_]*)/, "");
  2334. chunk.before = chunk.before.replace(/(\s?)$/, "");
  2335. var whitespace = re.$1;
  2336. chunk.before = chunk.before + starsAfter + whitespace;
  2337. }
  2338. else {
  2339. // In most cases, if you don't have any selected text and click the button
  2340. // you'll get a selected, marked up region with the default text inserted.
  2341. if (!chunk.selection && !starsAfter) {
  2342. chunk.selection = insertText;
  2343. }
  2344. // Add the true markup.
  2345. var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
  2346. chunk.before = chunk.before + markup;
  2347. chunk.after = markup + chunk.after;
  2348. }
  2349. return;
  2350. };
  2351. commandProto.stripLinkDefs = function (text, defsToAdd) {
  2352. text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
  2353. function (totalMatch, id, link, newlines, title) {
  2354. defsToAdd[id] = totalMatch.replace(/\s*$/, "");
  2355. if (newlines) {
  2356. // Strip the title and return that separately.
  2357. defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
  2358. return newlines + title;
  2359. }
  2360. return "";
  2361. });
  2362. return text;
  2363. };
  2364. commandProto.addLinkDef = function (chunk, linkDef) {
  2365. var refNumber = 0; // The current reference number
  2366. var defsToAdd = {}; //
  2367. // Start with a clean slate by removing all previous link definitions.
  2368. chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
  2369. chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
  2370. chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
  2371. var defs = "";
  2372. var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
  2373. var addDefNumber = function (def) {
  2374. refNumber++;
  2375. def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:");
  2376. defs += "\n" + def;
  2377. };
  2378. // note that
  2379. // a) the recursive call to getLink cannot go infinite, because by definition
  2380. // of regex, inner is always a proper substring of wholeMatch, and
  2381. // b) more than one level of nesting is neither supported by the regex
  2382. // nor making a lot of sense (the only use case for nesting is a linked image)
  2383. var getLink = function (wholeMatch, before, inner, afterInner, id, end) {
  2384. inner = inner.replace(regex, getLink);
  2385. if (defsToAdd[id]) {
  2386. addDefNumber(defsToAdd[id]);
  2387. return before + inner + afterInner + refNumber + end;
  2388. }
  2389. return wholeMatch;
  2390. };
  2391. chunk.before = chunk.before.replace(regex, getLink);
  2392. if (linkDef) {
  2393. addDefNumber(linkDef);
  2394. }
  2395. else {
  2396. chunk.selection = chunk.selection.replace(regex, getLink);
  2397. }
  2398. var refOut = refNumber;
  2399. chunk.after = chunk.after.replace(regex, getLink);
  2400. if (chunk.after) {
  2401. chunk.after = chunk.after.replace(/\n*$/, "");
  2402. }
  2403. if (!chunk.after) {
  2404. chunk.selection = chunk.selection.replace(/\n*$/, "");
  2405. }
  2406. chunk.after += "\n\n" + defs;
  2407. return refOut;
  2408. };
  2409. // takes the line as entered into the add link/as image dialog and makes
  2410. // sure the URL and the optinal title are "nice".
  2411. function properlyEncoded(linkdef) {
  2412. return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
  2413. link = link.replace(/\?.*$/, function (querypart) {
  2414. return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical
  2415. });
  2416. link = decodeURIComponent(link); // unencode first, to prevent double encoding
  2417. link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
  2418. link = link.replace(/\?.*$/, function (querypart) {
  2419. return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded
  2420. });
  2421. if (title) {
  2422. title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
  2423. title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  2424. }
  2425. return title ? link + ' "' + title + '"' : link;
  2426. });
  2427. }
  2428. commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
  2429. chunk.trimWhitespace();
  2430. chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
  2431. var background;
  2432. if (chunk.endTag.length > 1 && chunk.startTag.length > 0) {
  2433. chunk.startTag = chunk.startTag.replace(/!?\[/, "");
  2434. chunk.endTag = "";
  2435. this.addLinkDef(chunk, null);
  2436. }
  2437. else {
  2438. // We're moving start and end tag back into the selection, since (as we're in the else block) we're not
  2439. // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
  2440. // link text. linkEnteredCallback takes care of escaping any brackets.
  2441. chunk.selection = chunk.startTag + chunk.selection + chunk.endTag;
  2442. chunk.startTag = chunk.endTag = "";
  2443. if (/\n\n/.test(chunk.selection)) {
  2444. this.addLinkDef(chunk, null);
  2445. return;
  2446. }
  2447. var that = this;
  2448. // The function to be executed when you enter a link and press OK or Cancel.
  2449. // Marks up the link and adds the ref.
  2450. var linkEnteredCallback = function (link) {
  2451. if (link !== null) {
  2452. // ( $1
  2453. // [^\\] anything that's not a backslash
  2454. // (?:\\\\)* an even number (this includes zero) of backslashes
  2455. // )
  2456. // (?= followed by
  2457. // [[\]] an opening or closing bracket
  2458. // )
  2459. //
  2460. // In other words, a non-escaped bracket. These have to be escaped now to make sure they
  2461. // don't count as the end of the link or similar.
  2462. // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets),
  2463. // the bracket in one match may be the "not a backslash" character in the next match, so it
  2464. // should not be consumed by the first match.
  2465. // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the
  2466. // start of the string, so this also works if the selection begins with a bracket. We cannot solve
  2467. // this by anchoring with ^, because in the case that the selection starts with two brackets, this
  2468. // would mean a zero-width match at the start. Since zero-width matches advance the string position,
  2469. // the first bracket could then not act as the "not a backslash" for the second.
  2470. chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
  2471. var linkDef = " [999]: " + properlyEncoded(link);
  2472. var num = that.addLinkDef(chunk, linkDef);
  2473. chunk.startTag = isImage ? "![" : "[";
  2474. chunk.endTag = "][" + num + "]";
  2475. if (!chunk.selection) {
  2476. if (isImage) {
  2477. chunk.selection = "enter image description here";
  2478. }
  2479. else {
  2480. chunk.selection = "enter link description here";
  2481. }
  2482. }
  2483. }
  2484. postProcessing();
  2485. };
  2486. if (isImage) {
  2487. if (!this.hooks.insertImageDialog(linkEnteredCallback))
  2488. ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback);
  2489. }
  2490. else {
  2491. ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback);
  2492. }
  2493. return true;
  2494. }
  2495. };
  2496. // When making a list, hitting shift-enter will put your cursor on the next line
  2497. // at the current indent level.
  2498. commandProto.doAutoindent = function (chunk, postProcessing) {
  2499. var commandMgr = this,
  2500. fakeSelection = false;
  2501. chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
  2502. chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
  2503. chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
  2504. // There's no selection, end the cursor wasn't at the end of the line:
  2505. // The user wants to split the current list item / code line / blockquote line
  2506. // (for the latter it doesn't really matter) in two. Temporarily select the
  2507. // (rest of the) line to achieve this.
  2508. if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
  2509. chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
  2510. chunk.selection = wholeMatch;
  2511. return "";
  2512. });
  2513. fakeSelection = true;
  2514. }
  2515. if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
  2516. if (commandMgr.doList) {
  2517. commandMgr.doList(chunk);
  2518. }
  2519. }
  2520. if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
  2521. if (commandMgr.doBlockquote) {
  2522. commandMgr.doBlockquote(chunk);
  2523. }
  2524. }
  2525. if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
  2526. if (commandMgr.doCode) {
  2527. commandMgr.doCode(chunk);
  2528. }
  2529. }
  2530. if (fakeSelection) {
  2531. chunk.after = chunk.selection + chunk.after;
  2532. chunk.selection = "";
  2533. }
  2534. };
  2535. commandProto.doBlockquote = function (chunk, postProcessing) {
  2536. chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
  2537. function (totalMatch, newlinesBefore, text, newlinesAfter) {
  2538. chunk.before += newlinesBefore;
  2539. chunk.after = newlinesAfter + chunk.after;
  2540. return text;
  2541. });
  2542. chunk.before = chunk.before.replace(/(>[ \t]*)$/,
  2543. function (totalMatch, blankLine) {
  2544. chunk.selection = blankLine + chunk.selection;
  2545. return "";
  2546. });
  2547. chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
  2548. chunk.selection = chunk.selection || "Blockquote";
  2549. // The original code uses a regular expression to find out how much of the
  2550. // text *directly before* the selection already was a blockquote:
  2551. /*
  2552. if (chunk.before) {
  2553. chunk.before = chunk.before.replace(/\n?$/, "\n");
  2554. }
  2555. chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
  2556. function (totalMatch) {
  2557. chunk.startTag = totalMatch;
  2558. return "";
  2559. });
  2560. */
  2561. // This comes down to:
  2562. // Go backwards as many lines a possible, such that each line
  2563. // a) starts with ">", or
  2564. // b) is almost empty, except for whitespace, or
  2565. // c) is preceeded by an unbroken chain of non-empty lines
  2566. // leading up to a line that starts with ">" and at least one more character
  2567. // and in addition
  2568. // d) at least one line fulfills a)
  2569. //
  2570. // Since this is essentially a backwards-moving regex, it's susceptible to
  2571. // catstrophic backtracking and can cause the browser to hang;
  2572. // see e.g. http://meta.stackoverflow.com/questions/9807.
  2573. //
  2574. // Hence we replaced this by a simple state machine that just goes through the
  2575. // lines and checks for a), b), and c).
  2576. var match = "",
  2577. leftOver = "",
  2578. line;
  2579. if (chunk.before) {
  2580. var lines = chunk.before.replace(/\n$/, "").split("\n");
  2581. var inChain = false;
  2582. for (var i = 0; i < lines.length; i++) {
  2583. var good = false;
  2584. line = lines[i];
  2585. inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
  2586. if (/^>/.test(line)) { // a)
  2587. good = true;
  2588. if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain
  2589. inChain = true;
  2590. } else if (/^[ \t]*$/.test(line)) { // b)
  2591. good = true;
  2592. } else {
  2593. 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
  2594. }
  2595. if (good) {
  2596. match += line + "\n";
  2597. } else {
  2598. leftOver += match + line;
  2599. match = "\n";
  2600. }
  2601. }
  2602. if (!/(^|\n)>/.test(match)) { // d)
  2603. leftOver += match;
  2604. match = "";
  2605. }
  2606. }
  2607. chunk.startTag = match;
  2608. chunk.before = leftOver;
  2609. // end of change
  2610. if (chunk.after) {
  2611. chunk.after = chunk.after.replace(/^\n?/, "\n");
  2612. }
  2613. chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
  2614. function (totalMatch) {
  2615. chunk.endTag = totalMatch;
  2616. return "";
  2617. }
  2618. );
  2619. var replaceBlanksInTags = function (useBracket) {
  2620. var replacement = useBracket ? "> " : "";
  2621. if (chunk.startTag) {
  2622. chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
  2623. function (totalMatch, markdown) {
  2624. return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
  2625. });
  2626. }
  2627. if (chunk.endTag) {
  2628. chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
  2629. function (totalMatch, markdown) {
  2630. return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
  2631. });
  2632. }
  2633. };
  2634. if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
  2635. this.wrap(chunk, SETTINGS.lineLength - 2);
  2636. chunk.selection = chunk.selection.replace(/^/gm, "> ");
  2637. replaceBlanksInTags(true);
  2638. chunk.skipLines();
  2639. } else {
  2640. chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
  2641. this.unwrap(chunk);
  2642. replaceBlanksInTags(false);
  2643. if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
  2644. chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
  2645. }
  2646. if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
  2647. chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
  2648. }
  2649. }
  2650. chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
  2651. if (!/\n/.test(chunk.selection)) {
  2652. chunk.selection = chunk.selection.replace(/^(> *)/,
  2653. function (wholeMatch, blanks) {
  2654. chunk.startTag += blanks;
  2655. return "";
  2656. });
  2657. }
  2658. };
  2659. commandProto.doCode = function (chunk, postProcessing) {
  2660. var hasTextBefore = /\S[ ]*$/.test(chunk.before);
  2661. var hasTextAfter = /^[ ]*\S/.test(chunk.after);
  2662. // Use 'four space' markdown if the selection is on its own
  2663. // line or is multiline.
  2664. if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
  2665. chunk.before = chunk.before.replace(/[ ]{4}$/,
  2666. function (totalMatch) {
  2667. chunk.selection = totalMatch + chunk.selection;
  2668. return "";
  2669. });
  2670. var nLinesBack = 1;
  2671. var nLinesForward = 1;
  2672. if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
  2673. nLinesBack = 0;
  2674. }
  2675. if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
  2676. nLinesForward = 0;
  2677. }
  2678. chunk.skipLines(nLinesBack, nLinesForward);
  2679. if (!chunk.selection) {
  2680. chunk.startTag = " ";
  2681. chunk.selection = "enter code here";
  2682. }
  2683. else {
  2684. if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
  2685. if (/\n/.test(chunk.selection))
  2686. chunk.selection = chunk.selection.replace(/^/gm, " ");
  2687. else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
  2688. chunk.before += " ";
  2689. }
  2690. else {
  2691. chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
  2692. }
  2693. }
  2694. }
  2695. else {
  2696. // Use backticks (`) to delimit the code block.
  2697. chunk.trimWhitespace();
  2698. chunk.findTags(/`/, /`/);
  2699. if (!chunk.startTag && !chunk.endTag) {
  2700. chunk.startTag = chunk.endTag = "`";
  2701. if (!chunk.selection) {
  2702. chunk.selection = "enter code here";
  2703. }
  2704. }
  2705. else if (chunk.endTag && !chunk.startTag) {
  2706. chunk.before += chunk.endTag;
  2707. chunk.endTag = "";
  2708. }
  2709. else {
  2710. chunk.startTag = chunk.endTag = "";
  2711. }
  2712. }
  2713. };
  2714. commandProto.doList = function (chunk, postProcessing, isNumberedList) {
  2715. // These are identical except at the very beginning and end.
  2716. // Should probably use the regex extension function to make this clearer.
  2717. var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
  2718. var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
  2719. // The default bullet is a dash but others are possible.
  2720. // This has nothing to do with the particular HTML bullet,
  2721. // it's just a markdown bullet.
  2722. var bullet = "-";
  2723. // The number in a numbered list.
  2724. var num = 1;
  2725. // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
  2726. var getItemPrefix = function () {
  2727. var prefix;
  2728. if (isNumberedList) {
  2729. prefix = " " + num + ". ";
  2730. num++;
  2731. }
  2732. else {
  2733. prefix = " " + bullet + " ";
  2734. }
  2735. return prefix;
  2736. };
  2737. // Fixes the prefixes of the other list items.
  2738. var getPrefixedItem = function (itemText) {
  2739. // The numbering flag is unset when called by autoindent.
  2740. if (isNumberedList === undefined) {
  2741. isNumberedList = /^\s*\d/.test(itemText);
  2742. }
  2743. // Renumber/bullet the list element.
  2744. itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
  2745. function (_) {
  2746. return getItemPrefix();
  2747. });
  2748. return itemText;
  2749. };
  2750. chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
  2751. if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
  2752. chunk.before += chunk.startTag;
  2753. chunk.startTag = "";
  2754. }
  2755. if (chunk.startTag) {
  2756. var hasDigits = /\d+[.]/.test(chunk.startTag);
  2757. chunk.startTag = "";
  2758. chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
  2759. this.unwrap(chunk);
  2760. chunk.skipLines();
  2761. if (hasDigits) {
  2762. // Have to renumber the bullet points if this is a numbered list.
  2763. chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
  2764. }
  2765. if (isNumberedList == hasDigits) {
  2766. return;
  2767. }
  2768. }
  2769. var nLinesUp = 1;
  2770. chunk.before = chunk.before.replace(previousItemsRegex,
  2771. function (itemText) {
  2772. if (/^\s*([*+-])/.test(itemText)) {
  2773. bullet = re.$1;
  2774. }
  2775. nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
  2776. return getPrefixedItem(itemText);
  2777. });
  2778. if (!chunk.selection) {
  2779. chunk.selection = "List item";
  2780. }
  2781. var prefix = getItemPrefix();
  2782. var nLinesDown = 1;
  2783. chunk.after = chunk.after.replace(nextItemsRegex,
  2784. function (itemText) {
  2785. nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
  2786. return getPrefixedItem(itemText);
  2787. });
  2788. chunk.trimWhitespace(true);
  2789. chunk.skipLines(nLinesUp, nLinesDown, true);
  2790. chunk.startTag = prefix;
  2791. var spaces = prefix.replace(/./g, " ");
  2792. this.wrap(chunk, SETTINGS.lineLength - spaces.length);
  2793. chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
  2794. };
  2795. commandProto.doHeading = function (chunk, postProcessing) {
  2796. // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
  2797. chunk.selection = chunk.selection.replace(/\s+/g, " ");
  2798. chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
  2799. // If we clicked the button with no selected text, we just
  2800. // make a level 2 hash header around some default text.
  2801. if (!chunk.selection) {
  2802. chunk.startTag = "## ";
  2803. chunk.selection = "Heading";
  2804. chunk.endTag = " ##";
  2805. return;
  2806. }
  2807. var headerLevel = 0; // The existing header level of the selected text.
  2808. // Remove any existing hash heading markdown and save the header level.
  2809. chunk.findTags(/#+[ ]*/, /[ ]*#+/);
  2810. if (/#+/.test(chunk.startTag)) {
  2811. headerLevel = re.lastMatch.length;
  2812. }
  2813. chunk.startTag = chunk.endTag = "";
  2814. // Try to get the current header level by looking for - and = in the line
  2815. // below the selection.
  2816. chunk.findTags(null, /\s?(-+|=+)/);
  2817. if (/=+/.test(chunk.endTag)) {
  2818. headerLevel = 1;
  2819. }
  2820. if (/-+/.test(chunk.endTag)) {
  2821. headerLevel = 2;
  2822. }
  2823. // Skip to the next line so we can create the header markdown.
  2824. chunk.startTag = chunk.endTag = "";
  2825. chunk.skipLines(1, 1);
  2826. // We make a level 2 header if there is no current header.
  2827. // If there is a header level, we substract one from the header level.
  2828. // If it's already a level 1 header, it's removed.
  2829. var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
  2830. if (headerLevelToCreate > 0) {
  2831. // The button only creates level 1 and 2 underline headers.
  2832. // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner?
  2833. var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
  2834. var len = chunk.selection.length;
  2835. if (len > SETTINGS.lineLength) {
  2836. len = SETTINGS.lineLength;
  2837. }
  2838. chunk.endTag = "\n";
  2839. while (len--) {
  2840. chunk.endTag += headerChar;
  2841. }
  2842. }
  2843. };
  2844. commandProto.doHorizontalRule = function (chunk, postProcessing) {
  2845. chunk.startTag = "----------\n";
  2846. chunk.selection = "";
  2847. chunk.skipLines(2, 1, true);
  2848. }
  2849. })();
  2850. (function () {
  2851. var output, Converter;
  2852. if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
  2853. output = exports;
  2854. Converter = require("./Markdown.Converter").Converter;
  2855. } else {
  2856. output = window.Markdown;
  2857. Converter = output.Converter;
  2858. }
  2859. output.getSanitizingConverter = function () {
  2860. var converter = new Converter();
  2861. converter.hooks.chain("postConversion", sanitizeHtml);
  2862. converter.hooks.chain("postConversion", balanceTags);
  2863. return converter;
  2864. }
  2865. function sanitizeHtml(html) {
  2866. return html.replace(/<[^>]*>?/gi, sanitizeTag);
  2867. }
  2868. // (tags that can be opened/closed) | (tags that stand alone)
  2869. var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
  2870. // <a href="url..." optional title>|</a>
  2871. var a_white = /^(<a\shref="(https?:(\/\/|\/)|ftp:(\/\/|\/)|mailto:|magnet:)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
  2872. // <img src="url..." optional width optional height optional alt optional title
  2873. var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
  2874. // <pre optional class="prettyprint linenums">|</pre> for twitter bootstrap
  2875. var pre_white = /^(<pre(\sclass="prettyprint linenums")?>|<\/pre>)$/i;
  2876. function sanitizeTag(tag) {
  2877. if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white))
  2878. return tag;
  2879. else
  2880. return "";
  2881. }
  2882. /// <summary>
  2883. /// attempt to balance HTML tags in the html string
  2884. /// by removing any unmatched opening or closing tags
  2885. /// IMPORTANT: we *assume* HTML has *already* been
  2886. /// sanitized and is safe/sane before balancing!
  2887. ///
  2888. /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
  2889. /// </summary>
  2890. function balanceTags(html) {
  2891. if (html == "")
  2892. return "";
  2893. var re = /<\/?\w+[^>]*(\s|$|>)/g;
  2894. // convert everything to lower case; this makes
  2895. // our case insensitive comparisons easier
  2896. var tags = html.toLowerCase().match(re);
  2897. // no HTML tags present? nothing to do; exit now
  2898. var tagcount = (tags || []).length;
  2899. if (tagcount == 0)
  2900. return html;
  2901. var tagname, tag;
  2902. var ignoredtags = "<p><img><br><li><hr>";
  2903. var match;
  2904. var tagpaired = [];
  2905. var tagremove = [];
  2906. var needsRemoval = false;
  2907. // loop through matched tags in forward order
  2908. for (var ctag = 0; ctag < tagcount; ctag++) {
  2909. tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
  2910. // skip any already paired tags
  2911. // and skip tags in our ignore list; assume they're self-closed
  2912. if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
  2913. continue;
  2914. tag = tags[ctag];
  2915. match = -1;
  2916. if (!/^<\//.test(tag)) {
  2917. // this is an opening tag
  2918. // search forwards (next tags), look for closing tags
  2919. for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
  2920. if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
  2921. match = ntag;
  2922. break;
  2923. }
  2924. }
  2925. }
  2926. if (match == -1)
  2927. needsRemoval = tagremove[ctag] = true; // mark for removal
  2928. else
  2929. tagpaired[match] = true; // mark paired
  2930. }
  2931. if (!needsRemoval)
  2932. return html;
  2933. // delete all orphaned tags from the string
  2934. var ctag = 0;
  2935. html = html.replace(re, function (match) {
  2936. var res = tagremove[ctag] ? "" : match;
  2937. ctag++;
  2938. return res;
  2939. });
  2940. return html;
  2941. }
  2942. })();
  2943. /*
  2944. * Pagedown Bootstrap
  2945. * Author: Kevin O'Connor
  2946. * Version: 1.0
  2947. *
  2948. * Copyright (c) 2013 Kevin O'Connor
  2949. *
  2950. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  2951. * associated documentation files (the "Software"), to deal in the Software without restriction,
  2952. * including without limitation the rights to use, copy, modify, merge, publish, distribute,
  2953. * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  2954. * furnished to do so, subject to the following conditions:
  2955. *
  2956. * The above copyright notice and this permission notice shall be included in all copies or
  2957. * substantial portions of the Software.
  2958. *
  2959. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  2960. * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  2961. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  2962. * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  2963. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  2964. *
  2965. */
  2966. (function( $ ){
  2967. $.fn.pagedownBootstrap = function( options ) {
  2968. // Default settings
  2969. var settings = $.extend( {
  2970. 'sanitize' : true,
  2971. 'help' : null,
  2972. 'hooks' : Array()
  2973. }, options);
  2974. return this.each(function() {
  2975. //Setup converter
  2976. var converter = null;
  2977. if(settings.sanitize)
  2978. {
  2979. converter = Markdown.getSanitizingConverter();
  2980. } else {
  2981. converter = new Markdown.Converter()
  2982. }
  2983. //Register hooks
  2984. for(var i in settings.hooks)
  2985. {
  2986. var hook = settings.hooks[i];
  2987. if(typeof hook !== 'object' || typeof hook.event === 'undefined'
  2988. || typeof hook.callback !== 'function')
  2989. {
  2990. //A bad hook object was given
  2991. continue;
  2992. }
  2993. converter.hooks.chain(hook.event, hook.callback);
  2994. }
  2995. //Try to find a valid id for this element
  2996. var id = "wmd-input";
  2997. var idAppend = 0;
  2998. while($("#"+id+"-"+idAppend.toString()).length > 0)
  2999. {
  3000. idAppend++;
  3001. }
  3002. //Assign the choosen id to the element
  3003. $(this).attr('id', id+"-"+idAppend.toString());
  3004. //Wrap the element with the needed html
  3005. $(this).wrap('<div class="wmd-panel" />');
  3006. $(this).before('<div id="wmd-button-bar-'+idAppend+'" class="wmd-button-bar" />');
  3007. $(this).after('<div id="wmd-preview-'+idAppend+'" class="wmd-preview" />');
  3008. $(this).addClass('wmd-input');
  3009. //Setup help function
  3010. help = null;
  3011. if($.isFunction(settings.help))
  3012. {
  3013. help = { handler: settings.help };
  3014. }
  3015. //Setup editor
  3016. var editor = new Markdown.Editor(converter, "-"+idAppend.toString(), help);
  3017. editor.run();
  3018. });
  3019. };
  3020. })( jQuery );