Markdown.Editor.js 89 KB

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