diff --git a/.gitignore b/.gitignore
index 78c3518..4548843 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ database.sqlite-journal
vendor
database.sqlite
notehub
+.idea
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..b5c781f
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM golang:1.14.3-alpine
+WORKDIR /go/src/app
+RUN apk --no-cache add curl make sqlite gcc musl-dev git
+
+RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
+COPY . .
+RUN dep ensure
+
+RUN make db
+EXPOSE 3000
+CMD ["make", "run"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 8329abd..f4dfc70 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,9 @@
run:
- TEST_MODE=1 go run *.go
+ go run *.go
tests:
go run test/main.go
db:
- echo 'CREATE TABLE "notes" (`id` VARCHAR(6) UNIQUE PRIMARY KEY, `text` TEXT, `published` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `edited` TIMESTAMP DEFAULT NULL, `password` VARCHAR(16), `views` INTEGER DEFAULT 0);' | sqlite3 database.sqlite
+ mkdir data
+ echo 'CREATE TABLE "notes" (`id` VARCHAR(6) UNIQUE PRIMARY KEY, `text` TEXT, `published` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `edited` TIMESTAMP DEFAULT NULL, `password` VARCHAR(16), `views` INTEGER DEFAULT 0);' | sqlite3 data/database.sqlite
diff --git a/assets/markdown/TOS.md b/assets/markdown/TOS.md
deleted file mode 100644
index aee7647..0000000
--- a/assets/markdown/TOS.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Terms of Service
-
-### 1. Terms
-
-By accessing the web site at https://notehub.org, you are agreeing to be
-bound by these web site Terms and Conditions of Use, all applicable laws and
-regulations, and agree that you are responsible for compliance with any
-applicable local laws. If you do not agree with any of these terms, you are
-prohibited from using or accessing this site. The materials contained in this
-web site are protected by applicable copyright and trademark law.
-
-### 2. Use License
-
-a. Under this license you may not:
- 1. attempt to __modify, manipulate__ or __extend__ any software contained on NoteHub's web site;
- 2. create _any_ kind of content unlawful to __possess__, __produce__ or __distribute__ under your local law;
- 3. create 'hop' notes containing a collection of links to external resources;
-
-b. This license shall automatically terminate if you violate any of these
-restrictions and may be terminated by NoteHub at any time. Upon terminating
-your viewing of these materials or upon the termination of this license, you
-must destroy any downloaded materials in your possession whether in electronic
-or printed format.
-
-### 3. Disclaimer
-
-a. The materials on NoteHub's web site are provided "as is". NoteHub makes no
-warranties, expressed or implied, and hereby disclaims and negates all other
-warranties, including without limitation, implied warranties or conditions of
-merchantability, fitness for a particular purpose, or non-infringement of
-intellectual property or other violation of rights.
-
-Further, NoteHub does not warrant or make any representations concerning the
-accuracy, likely results, or reliability of the use of the materials on its
-Internet web site or otherwise relating to such materials or on any sites
-linked to this site.
-
-### 4. Limitations
-
-In no event shall NoteHub or its suppliers be liable for any damages
-(including, without limitation, damages for loss of data or profit, or due to
-business interruption,) arising out of the use or inability to use the
-materials on NoteHub's Internet site, even if NoteHub or a NoteHub authorized
-representative has been notified orally or in writing of the possibility of
-such damage. Because some jurisdictions do not allow limitations on implied
-warranties, or limitations of liability for consequential or incidental
-damages, these limitations may not apply to you.
-
-### 5. Revisions and Errata
-
-The materials appearing on NoteHub's web site could include technical,
-typographical, or photographic errors. NoteHub does not warrant that any of the
-materials on its web site are accurate, complete, or current. NoteHub may make
-changes to the materials contained on its web site at any time without notice.
-NoteHub does not, however, make any commitment to update the materials.
-
-### 6. Links
-
-NoteHub has not reviewed all of the sites linked to its Internet web site and
-is not responsible for the contents of any such linked site. The inclusion of
-any link does not imply endorsement by NoteHub of the site. Use of any such
-linked web site is at the user's own risk.
-
-### 7. Site Terms of Use Modifications
-
-NoteHub may revise these terms of use for its web site at any time without
-notice. By using this web site you are agreeing to be bound by the then
-current version of these Terms and Conditions of Use.
-
-### 8. Governing Law
-
-Any claim relating to NoteHub's web site shall be governed by the laws of
-Germany without regard to its conflict of law provisions.
-
-General Terms and Conditions applicable to Use of a Web Site.
-
diff --git a/assets/public/editor/Markdown.Converter.js b/assets/public/editor/Markdown.Converter.js
new file mode 100644
index 0000000..47fe636
--- /dev/null
+++ b/assets/public/editor/Markdown.Converter.js
@@ -0,0 +1,1631 @@
+//modification JB: \pagebreak
+
+"use strict";
+var Markdown;
+
+if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module
+ Markdown = exports;
+else
+ Markdown = {};
+
+// The following text is included for historical reasons, but should
+// be taken with a pinch of salt; it's not all true anymore.
+
+//
+// Wherever possible, Showdown is a straight, line-by-line port
+// of the Perl version of Markdown.
+//
+// This is not a normal parser design; it's basically just a
+// series of string substitutions. It's hard to read and
+// maintain this way, but keeping Showdown close to the original
+// design makes it easier to port new features.
+//
+// More importantly, Showdown behaves like markdown.pl in most
+// edge cases. So web applications can do client-side preview
+// in Javascript, and then build identical HTML on the server.
+//
+// This port needs the new RegExp functionality of ECMA 262,
+// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers
+// should do fine. Even with the new regular expression features,
+// We do a lot of work to emulate Perl's regex functionality.
+// The tricky changes in this file mostly have the "attacklab:"
+// label. Major or self-explanatory changes don't.
+//
+// Smart diff tools like Araxis Merge will be able to match up
+// this file with markdown.pl in a useful way. A little tweaking
+// helps: in a copy of markdown.pl, replace "#" with "//" and
+// replace "$text" with "text". Be sure to ignore whitespace
+// and line endings.
+//
+
+
+//
+// Usage:
+//
+// var text = "Markdown *rocks*.";
+//
+// var converter = new Markdown.Converter();
+// var html = converter.makeHtml(text);
+//
+// alert(html);
+//
+// Note: move the sample code to the bottom of this
+// file before uncommenting it.
+//
+
+(function () {
+
+ function identity(x) { return x; }
+ function returnFalse(x) { return false; }
+
+ function HookCollection() { }
+
+ HookCollection.prototype = {
+
+ chain: function (hookname, func) {
+ var original = this[hookname];
+ if (!original)
+ throw new Error("unknown hook " + hookname);
+
+ if (original === identity)
+ this[hookname] = func;
+ else
+ this[hookname] = function (text) {
+ var args = Array.prototype.slice.call(arguments, 0);
+ args[0] = original.apply(null, args);
+ return func.apply(null, args);
+ };
+ },
+ set: function (hookname, func) {
+ if (!this[hookname])
+ throw new Error("unknown hook " + hookname);
+ this[hookname] = func;
+ },
+ addNoop: function (hookname) {
+ this[hookname] = identity;
+ },
+ addFalse: function (hookname) {
+ this[hookname] = returnFalse;
+ }
+ };
+
+ Markdown.HookCollection = HookCollection;
+
+ // g_urls and g_titles allow arbitrary user-entered strings as keys. This
+ // caused an exception (and hence stopped the rendering) when the user entered
+ // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this
+ // (since no builtin property starts with "s_"). See
+ // http://meta.stackexchange.com/questions/64655/strange-wmd-bug
+ // (granted, switching from Array() to Object() alone would have left only __proto__
+ // to be a problem)
+ function SaveHash() { }
+ SaveHash.prototype = {
+ set: function (key, value) {
+ this["s_" + key] = value;
+ },
+ get: function (key) {
+ return this["s_" + key];
+ }
+ };
+
+ Markdown.Converter = function (OPTIONS) {
+ var pluginHooks = this.hooks = new HookCollection();
+
+ // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link
+ pluginHooks.addNoop("plainLinkText");
+
+ // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked
+ pluginHooks.addNoop("preConversion");
+
+ // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have
+ pluginHooks.addNoop("postNormalization");
+
+ // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively
+ // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner
+ // call will receive outdented text.
+ pluginHooks.addNoop("preBlockGamut");
+ pluginHooks.addNoop("postBlockGamut");
+
+ // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made
+ pluginHooks.addNoop("preSpanGamut");
+ pluginHooks.addNoop("postSpanGamut");
+
+ // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml
+ pluginHooks.addNoop("postConversion");
+
+ //
+ // Private state of the converter instance:
+ //
+
+ // Global hashes, used by various utility routines
+ var g_urls;
+ var g_titles;
+ var g_html_blocks;
+
+ // Used to track when we're inside an ordered or unordered list
+ // (see _ProcessListItems() for details):
+ var g_list_level;
+
+ OPTIONS = OPTIONS || {};
+ var asciify = identity, deasciify = identity;
+ if (OPTIONS.nonAsciiLetters) {
+
+ /* In JavaScript regular expressions, \w only denotes [a-zA-Z0-9_].
+ * That's why there's inconsistent handling e.g. with intra-word bolding
+ * of Japanese words. That's why we do the following if OPTIONS.nonAsciiLetters
+ * is true:
+ *
+ * Before doing bold and italics, we find every instance
+ * of a unicode word character in the Markdown source that is not
+ * matched by \w, and the letter "Q". We take the character's code point
+ * and encode it in base 51, using the "digits"
+ *
+ * A, B, ..., P, R, ..., Y, Z, a, b, ..., y, z
+ *
+ * delimiting it with "Q" on both sides. For example, the source
+ *
+ * > In Chinese, the smurfs are called 藍精靈, meaning "blue spirits".
+ *
+ * turns into
+ *
+ * > In Chinese, the smurfs are called QNIhQQMOIQQOuUQ, meaning "blue spirits".
+ *
+ * Since everything that is a letter in Unicode is now a letter (or
+ * several letters) in ASCII, \w and \b should always do the right thing.
+ *
+ * After the bold/italic conversion, we decode again; since "Q" was encoded
+ * alongside all non-ascii characters (as "QBfQ"), and the conversion
+ * will not generate "Q", the only instances of that letter should be our
+ * encoded characters. And since the conversion will not break words, the
+ * "Q...Q" should all still be in one piece.
+ *
+ * We're using "Q" as the delimiter because it's probably one of the
+ * rarest characters, and also because I can't think of any special behavior
+ * that would ever be triggered by this letter (to use a silly example, if we
+ * delimited with "H" on the left and "P" on the right, then "Ψ" would be
+ * encoded as "HTTP", which may cause special behavior). The latter would not
+ * actually be a huge issue for bold/italic, but may be if we later use it
+ * in other places as well.
+ * */
+ (function () {
+ var lettersThatJavaScriptDoesNotKnowAndQ = /[Q\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/g;
+ var cp_Q = "Q".charCodeAt(0);
+ var cp_A = "A".charCodeAt(0);
+ var cp_Z = "Z".charCodeAt(0);
+ var dist_Za = "a".charCodeAt(0) - cp_Z - 1;
+
+ asciify = function(text) {
+ return text.replace(lettersThatJavaScriptDoesNotKnowAndQ, function (m) {
+ var c = m.charCodeAt(0);
+ var s = "";
+ var v;
+ while (c > 0) {
+ v = (c % 51) + cp_A;
+ if (v >= cp_Q)
+ v++;
+ if (v > cp_Z)
+ v += dist_Za;
+ s = String.fromCharCode(v) + s;
+ c = c / 51 | 0;
+ }
+ return "Q" + s + "Q";
+ })
+ };
+
+ deasciify = function(text) {
+ return text.replace(/Q([A-PR-Za-z]{1,3})Q/g, function (m, s) {
+ var c = 0;
+ var v;
+ for (var i = 0; i < s.length; i++) {
+ v = s.charCodeAt(i);
+ if (v > cp_Z)
+ v -= dist_Za;
+ if (v > cp_Q)
+ v--;
+ v -= cp_A;
+ c = (c * 51) + v;
+ }
+ return String.fromCharCode(c);
+ })
+ }
+ })();
+ }
+
+ var _DoItalicsAndBold = OPTIONS.asteriskIntraWordEmphasis ? _DoItalicsAndBold_AllowIntrawordWithAsterisk : _DoItalicsAndBoldStrict;
+
+ this.makeHtml = function (text) {
+
+ //
+ // Main function. The order in which other subs are called here is
+ // essential. Link and image substitutions need to happen before
+ // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the
+ // and tags get encoded.
+ //
+
+ // This will only happen if makeHtml on the same converter instance is called from a plugin hook.
+ // Don't do that.
+ if (g_urls)
+ throw new Error("Recursive call to converter.makeHtml");
+
+ // Create the private state objects.
+ g_urls = new SaveHash();
+ g_titles = new SaveHash();
+ g_html_blocks = [];
+ g_list_level = 0;
+
+ text = pluginHooks.preConversion(text);
+
+ // attacklab: Replace ~ with ~T
+ // This lets us use tilde as an escape char to avoid md5 hashes
+ // The choice of character is arbitray; anything that isn't
+ // magic in Markdown will work.
+ text = text.replace(/~/g, "~T");
+
+ // attacklab: Replace $ with ~D
+ // RegExp interprets $ as a special character
+ // when it's in a replacement string
+ text = text.replace(/\$/g, "~D");
+
+ // Standardize line endings
+ text = text.replace(/\r\n/g, "\n"); // DOS to Unix
+ text = text.replace(/\r/g, "\n"); // Mac to Unix
+
+ // Make sure text begins and ends with a couple of newlines:
+ text = "\n\n" + text + "\n\n";
+
+ // Convert all tabs to spaces.
+ text = _Detab(text);
+
+ // Strip any lines consisting only of spaces and tabs.
+ // This makes subsequent regexen easier to write, because we can
+ // match consecutive blank lines with /\n+/ instead of something
+ // contorted like /[ \t]*\n+/ .
+ text = text.replace(/^[ \t]+$/mg, "");
+
+ text = pluginHooks.postNormalization(text);
+
+ // Turn block-level HTML blocks into hash entries
+ text = _HashHTMLBlocks(text);
+
+ // Strip link definitions, store in hashes.
+ text = _StripLinkDefinitions(text);
+
+ text = _RunBlockGamut(text);
+
+ text = _UnescapeSpecialChars(text);
+
+ // attacklab: Restore dollar signs
+ text = text.replace(/~D/g, "$$");
+
+ // attacklab: Restore tildes
+ text = text.replace(/~T/g, "~");
+
+ text = pluginHooks.postConversion(text);
+
+ text = text.replace(/\\pagebreak/g, '');
+
+ g_html_blocks = g_titles = g_urls = null;
+
+ return text;
+ };
+
+ function _StripLinkDefinitions(text) {
+ //
+ // Strips link definitions from text, stores the URLs and titles in
+ // hash references.
+ //
+
+ // Link defs are in the form: ^[id]: url "optional title"
+
+ /*
+ text = text.replace(/
+ ^[ ]{0,3}\[([^\[\]]+)\]: // id = $1 attacklab: g_tab_width - 1
+ [ \t]*
+ \n? // maybe *one* newline
+ [ \t]*
+ (\S+?)>? // url = $2
+ (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below
+ [ \t]*
+ \n? // maybe one newline
+ [ \t]*
+ ( // (potential) title = $3
+ (\n*) // any lines skipped = $4 attacklab: lookbehind removed
+ [ \t]+
+ ["(]
+ (.+?) // title = $5
+ [")]
+ [ \t]*
+ )? // title is optional
+ (?:\n+|$)
+ /gm, function(){...});
+ */
+
+ text = text.replace(/^[ ]{0,3}\[([^\[\]]+)\]:[ \t]*\n?[ \t]*(\S+?)>?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm,
+ function (wholeMatch, m1, m2, m3, m4, m5) {
+ m1 = m1.toLowerCase();
+ g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive
+ if (m4) {
+ // Oops, found blank lines, so it's not a title.
+ // Put back the parenthetical statement we stole.
+ return m3;
+ } else if (m5) {
+ g_titles.set(m1, m5.replace(/"/g, """));
+ }
+
+ // Completely remove the definition from the text
+ return "";
+ }
+ );
+
+ return text;
+ }
+
+ function _HashHTMLBlocks(text) {
+
+ // Hashify HTML blocks:
+ // We only want to do this for block-level HTML tags, such as headers,
+ // lists, and tables. That's because we still want to wrap
\n\n"; }
+ );
+
+ text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
+ function (matchFound, m1) { return "
" + _RunSpanGamut(m1) + "
\n\n"; }
+ );
+
+ // atx-style headers:
+ // # Header 1
+ // ## Header 2
+ // ## Header 2 with closing hashes ##
+ // ...
+ // ###### Header 6
+ //
+
+ /*
+ text = text.replace(/
+ ^(\#{1,6}) // $1 = string of #'s
+ [ \t]*
+ (.+?) // $2 = Header text
+ [ \t]*
+ \#* // optional closing #'s (not counted)
+ \n+
+ /gm, function() {...});
+ */
+
+ text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
+ function (wholeMatch, m1, m2) {
+ var h_level = m1.length;
+ return "" + _RunSpanGamut(m2) + "\n\n";
+ }
+ );
+
+ return text;
+ }
+
+ function _DoLists(text, isInsideParagraphlessListItem) {
+ //
+ // Form HTML ordered (numbered) and unordered (bulleted) lists.
+ //
+
+ // attacklab: add sentinel to hack around khtml/safari bug:
+ // http://bugs.webkit.org/show_bug.cgi?id=11231
+ text += "~0";
+
+ // Re-usable pattern to match any entirel ul or ol list:
+
+ /*
+ var whole_list = /
+ ( // $1 = whole list
+ ( // $2
+ [ ]{0,3} // attacklab: g_tab_width - 1
+ ([*+-]|\d+[.]) // $3 = first list item marker
+ [ \t]+
+ )
+ [^\r]+?
+ ( // $4
+ ~0 // sentinel for workaround; should be $
+ |
+ \n{2,}
+ (?=\S)
+ (?! // Negative lookahead for another list item marker
+ [ \t]*
+ (?:[*+-]|\d+[.])[ \t]+
+ )
+ )
+ )
+ /g
+ */
+ var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
+ if (g_list_level) {
+ text = text.replace(whole_list, function (wholeMatch, m1, m2) {
+ var list = m1;
+ var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol";
+ var first_number;
+ if (list_type === "ol")
+ first_number = parseInt(m2, 10)
+
+ var result = _ProcessListItems(list, list_type, isInsideParagraphlessListItem);
+
+ // Trim any trailing whitespace, to put the closing `$list_type>`
+ // up on the preceding line, to get it past the current stupid
+ // HTML block parser. This is a hack to work around the terrible
+ // hack that is the HTML block parser.
+ result = result.replace(/\s+$/, "");
+ var opening = "<" + list_type;
+ if (first_number && first_number !== 1)
+ opening += " start=\"" + first_number + "\"";
+ result = opening + ">" + result + "" + list_type + ">\n";
+ return result;
+ });
+ } else {
+ whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;
+ text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) {
+ var runup = m1;
+ var list = m2;
+
+ var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol";
+
+ var first_number;
+ if (list_type === "ol")
+ first_number = parseInt(m3, 10)
+
+ var result = _ProcessListItems(list, list_type);
+ var opening = "<" + list_type;
+ if (first_number && first_number !== 1)
+ opening += " start=\"" + first_number + "\"";
+
+ result = runup + opening + ">\n" + result + "" + list_type + ">\n";
+ return result;
+ });
+ }
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/, "");
+
+ return text;
+ }
+
+ var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" };
+
+ function _ProcessListItems(list_str, list_type, isInsideParagraphlessListItem) {
+ //
+ // Process the contents of a single ordered or unordered list, splitting it
+ // into individual list items.
+ //
+ // list_type is either "ul" or "ol".
+
+ // The $g_list_level global keeps track of when we're inside a list.
+ // Each time we enter a list, we increment it; when we leave a list,
+ // we decrement. If it's zero, we're not in a list anymore.
+ //
+ // We do this because when we're not inside a list, we want to treat
+ // something like this:
+ //
+ // I recommend upgrading to version
+ // 8. Oops, now this line is treated
+ // as a sub-list.
+ //
+ // As a single paragraph, despite the fact that the second line starts
+ // with a digit-period-space sequence.
+ //
+ // Whereas when we're inside a list (or sub-list), that line will be
+ // treated as the start of a sub-list. What a kludge, huh? This is
+ // an aspect of Markdown's syntax that's hard to parse perfectly
+ // without resorting to mind-reading. Perhaps the solution is to
+ // change the syntax rules such that sub-lists must start with a
+ // starting cardinal number; e.g. "1." or "a.".
+
+ g_list_level++;
+
+ // trim trailing blank lines:
+ list_str = list_str.replace(/\n{2,}$/, "\n");
+
+ // attacklab: add sentinel to emulate \z
+ list_str += "~0";
+
+ // In the original attacklab showdown, list_type was not given to this function, and anything
+ // that matched /[*+-]|\d+[.]/ would just create the next
, causing this mismatch:
+ //
+ // Markdown rendered by WMD rendered by MarkdownSharp
+ // ------------------------------------------------------------------
+ // 1. first 1. first 1. first
+ // 2. second 2. second 2. second
+ // - third 3. third * third
+ //
+ // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx,
+ // with {MARKER} being one of \d+[.] or [*+-], depending on list_type:
+
+ /*
+ list_str = list_str.replace(/
+ (^[ \t]*) // leading whitespace = $1
+ ({MARKER}) [ \t]+ // list marker = $2
+ ([^\r]+? // list item text = $3
+ (\n+)
+ )
+ (?=
+ (~0 | \2 ({MARKER}) [ \t]+)
+ )
+ /gm, function(){...});
+ */
+
+ var marker = _listItemMarkers[list_type];
+ var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm");
+ var last_item_had_a_double_newline = false;
+ list_str = list_str.replace(re,
+ function (wholeMatch, m1, m2, m3) {
+ var item = m3;
+ var leading_space = m1;
+ var ends_with_double_newline = /\n\n$/.test(item);
+ var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1;
+
+ if (contains_double_newline || last_item_had_a_double_newline) {
+ item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true);
+ }
+ else {
+ // Recursion for sub-lists:
+ item = _DoLists(_Outdent(item), /* isInsideParagraphlessListItem= */ true);
+ item = item.replace(/\n$/, ""); // chomp(item)
+ if (!isInsideParagraphlessListItem) // only the outer-most item should run this, otherwise it's run multiple times for the inner ones
+ item = _RunSpanGamut(item);
+ }
+ last_item_had_a_double_newline = ends_with_double_newline;
+ return "
` blocks.
+ //
+
+ /*
+ text = text.replace(/
+ (?:\n\n|^)
+ ( // $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
+ .*\n+
+ )+
+ )
+ (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
+ /g ,function(){...});
+ */
+
+ // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
+ text += "~0";
+
+ text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
+ function (wholeMatch, m1, m2) {
+ var codeblock = m1;
+ var nextChar = m2;
+
+ codeblock = _EncodeCode(_Outdent(codeblock));
+ codeblock = _Detab(codeblock);
+ codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
+ codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
+
+ codeblock = "
" + codeblock + "\n
";
+
+ return "\n\n" + codeblock + "\n\n" + nextChar;
+ }
+ );
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/, "");
+
+ return text;
+ }
+
+ function _DoCodeSpans(text) {
+ //
+ // * Backtick quotes are used for spans.
+ //
+ // * You can use multiple backticks as the delimiters if you want to
+ // include literal backticks in the code span. So, this input:
+ //
+ // Just type ``foo `bar` baz`` at the prompt.
+ //
+ // Will translate to:
+ //
+ //
Just type foo `bar` baz at the prompt.
+ //
+ // There's no arbitrary limit to the number of backticks you
+ // can use as delimters. If you need three consecutive backticks
+ // in your code, use four for delimiters, etc.
+ //
+ // * You can use spaces to get literal backticks at the edges:
+ //
+ // ... type `` `bar` `` ...
+ //
+ // Turns to:
+ //
+ // ... type `bar` ...
+ //
+
+ /*
+ text = text.replace(/
+ (^|[^\\`]) // Character before opening ` can't be a backslash or backtick
+ (`+) // $2 = Opening run of `
+ (?!`) // and no more backticks -- match the full run
+ ( // $3 = The code block
+ [^\r]*?
+ [^`] // attacklab: work around lack of lookbehind
+ )
+ \2 // Matching closer
+ (?!`)
+ /gm, function(){...});
+ */
+
+ text = text.replace(/(^|[^\\`])(`+)(?!`)([^\r]*?[^`])\2(?!`)/gm,
+ function (wholeMatch, m1, m2, m3, m4) {
+ var c = m3;
+ c = c.replace(/^([ \t]*)/g, ""); // leading whitespace
+ c = c.replace(/[ \t]*$/g, ""); // trailing whitespace
+ c = _EncodeCode(c);
+ 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.
+ return m1 + "" + c + "";
+ }
+ );
+
+ return text;
+ }
+
+ function _EncodeCode(text) {
+ //
+ // Encode/escape certain characters inside Markdown code runs.
+ // The point is that in code, these characters are literals,
+ // and lose their special Markdown meanings.
+ //
+ // Encode all ampersands; HTML entities are not
+ // entities within a Markdown code span.
+ text = text.replace(/&/g, "&");
+
+ // Do the angle bracket song and dance:
+ text = text.replace(//g, ">");
+
+ // Now, escape characters that are magic in Markdown:
+ text = escapeCharacters(text, "\*_{}[]\\", false);
+
+ // jj the line above breaks this:
+ //---
+
+ //* Item
+
+ // 1. Subitem
+
+ // special char: *
+ //---
+
+ return text;
+ }
+
+ function _DoItalicsAndBoldStrict(text) {
+
+ if (text.indexOf("*") === -1 && text.indexOf("_") === - 1)
+ return text;
+
+ text = asciify(text);
+
+ // must go first:
+
+ // (^|[\W_]) Start with a non-letter or beginning of string. Store in \1.
+ // (?:(?!\1)|(?=^)) Either the next character is *not* the same as the previous,
+ // or we started at the end of the string (in which case the previous
+ // group had zero width, so we're still there). Because the next
+ // character is the marker, this means that if there are e.g. multiple
+ // underscores in a row, we can only match the left-most ones (which
+ // prevents foo___bar__ from getting bolded)
+ // (\*|_) The marker character itself, asterisk or underscore. Store in \2.
+ // \2 The marker again, since bold needs two.
+ // (?=\S) The first bolded character cannot be a space.
+ // ([^\r]*?\S) The actual bolded string. At least one character, and it cannot *end*
+ // with a space either. Note that like in many other places, [^\r] is
+ // just a workaround for JS' lack of single-line regexes; it's equivalent
+ // to a . in an /s regex, because the string cannot contain any \r (they
+ // are removed in the normalizing step).
+ // \2\2 The marker character, twice -- end of bold.
+ // (?!\2) Not followed by another marker character (ensuring that we match the
+ // rightmost two in a longer row)...
+ // (?=[\W_]|$) ...but by any other non-word character or the end of string.
+ text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)([^\r]*?\S)\2\2(?!\2)(?=[\W_]|$)/g,
+ "$1$3");
+
+ // This is almost identical to the regex, except 1) there's obviously just one marker
+ // character, and 2) the italicized string cannot contain the marker character.
+ text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2)[^\r])*?\S)\2(?!\2)(?=[\W_]|$)/g,
+ "$1$3");
+
+ return deasciify(text);
+ }
+
+ function _DoItalicsAndBold_AllowIntrawordWithAsterisk(text) {
+
+ if (text.indexOf("*") === -1 && text.indexOf("_") === - 1)
+ return text;
+
+ text = asciify(text);
+
+ // must go first:
+ // (?=[^\r][*_]|[*_]) Optimization only, to find potentially relevant text portions faster. Minimally slower in Chrome, but much faster in IE.
+ // ( Store in \1. This is the last character before the delimiter
+ // ^ Either we're at the start of the string (i.e. there is no last character)...
+ // | ... or we allow one of the following:
+ // (?= (lookahead; we're not capturing this, just listing legal possibilities)
+ // \W__ If the delimiter is __, then this last character must be non-word non-underscore (extra-word emphasis only)
+ // |
+ // (?!\*)[\W_]\*\* If the delimiter is **, then this last character can be non-word non-asterisk (extra-word emphasis)...
+ // |
+ // \w\*\*\w ...or it can be word/underscore, but only if the first bolded character is such a character as well (intra-word emphasis)
+ // )
+ // [^\r] actually capture the character (can't use `.` since it could be \n)
+ // )
+ // (\*\*|__) Store in \2: the actual delimiter
+ // (?!\2) not followed by the delimiter again (at most one more asterisk/underscore is allowed)
+ // (?=\S) the first bolded character can't be a space
+ // ( Store in \3: the bolded string
+ //
+ // (?:| Look at all bolded characters except for the last one. Either that's empty, meaning only a single character was bolded...
+ // [^\r]*? ... otherwise take arbitrary characters, minimally matching; that's all bolded characters except for the last *two*
+ // (?!\2) the last two characters cannot be the delimiter itself (because that would mean four underscores/asterisks in a row)
+ // [^\r] capture the next-to-last bolded character
+ // )
+ // (?= lookahead at the very last bolded char and what comes after
+ // \S_ for underscore-bolding, it can be any non-space
+ // |
+ // \w for asterisk-bolding (otherwise the previous alternative would've matched, since \w implies \S), either the last char is word/underscore...
+ // |
+ // \S\*\*(?:[\W_]|$) ... or it's any other non-space, but in that case the character *after* the delimiter may not be a word character
+ // )
+ // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases)
+ // )
+ // (?= lookahead; list the legal possibilities for the closing delimiter and its following character
+ // __(?:\W|$) for underscore-bolding, the following character (if any) must be non-word non-underscore
+ // |
+ // \*\*(?:[^*]|$) for asterisk-bolding, any non-asterisk is allowed (note we already ensured above that it's not a word character if the last bolded character wasn't one)
+ // )
+ // \2 actually capture the closing delimiter (and make sure that it matches the opening one)
+
+ text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W__|(?!\*)[\W_]\*\*|\w\*\*\w)[^\r])(\*\*|__)(?!\2)(?=\S)((?:|[^\r]*?(?!\2)[^\r])(?=\S_|\w|\S\*\*(?:[\W_]|$)).)(?=__(?:\W|$)|\*\*(?:[^*]|$))\2/g,
+ "$1$3");
+
+ // now :
+ // (?=[^\r][*_]|[*_]) Optimization, see above.
+ // ( Store in \1. This is the last character before the delimiter
+ // ^ Either we're at the start of the string (i.e. there is no last character)...
+ // | ... or we allow one of the following:
+ // (?= (lookahead; we're not capturing this, just listing legal possibilities)
+ // \W_ If the delimiter is _, then this last character must be non-word non-underscore (extra-word emphasis only)
+ // |
+ // (?!\*) otherwise, we list two possiblities for * as the delimiter; in either case, the last characters cannot be an asterisk itself
+ // (?:
+ // [\W_]\* this last character can be non-word (extra-word emphasis)...
+ // |
+ // \D\*(?=\w)\D ...or it can be word (otherwise the first alternative would've matched), but only if
+ // a) the first italicized character is such a character as well (intra-word emphasis), and
+ // b) neither character on either side of the asterisk is a digit
+ // )
+ // )
+ // [^\r] actually capture the character (can't use `.` since it could be \n)
+ // )
+ // (\*|_) Store in \2: the actual delimiter
+ // (?!\2\2\2) not followed by more than two more instances of the delimiter
+ // (?=\S) the first italicized character can't be a space
+ // ( Store in \3: the italicized string
+ // (?:(?!\2)[^\r])*? arbitrary characters except for the delimiter itself, minimally matching
+ // (?= lookahead at the very last italicized char and what comes after
+ // [^\s_]_ for underscore-italicizing, it can be any non-space non-underscore
+ // |
+ // (?=\w)\D\*\D for asterisk-italicizing, either the last char is word/underscore *and* neither character on either side of the asterisk is a digit...
+ // |
+ // [^\s*]\*(?:[\W_]|$) ... or that last char is any other non-space non-asterisk, but then the character after the delimiter (if any) must be non-word
+ // )
+ // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases)
+ // )
+ // (?= lookahead; list the legal possibilities for the closing delimiter and its following character
+ // _(?:\W|$) for underscore-italicizing, the following character (if any) must be non-word non-underscore
+ // |
+ // \*(?:[^*]|$) for asterisk-italicizing, any non-asterisk is allowed; all other restrictions have already been ensured in the previous lookahead
+ // )
+ // \2 actually capture the closing delimiter (and make sure that it matches the opening one)
+
+ text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W_|(?!\*)(?:[\W_]\*|\D\*(?=\w)\D))[^\r])(\*|_)(?!\2\2\2)(?=\S)((?:(?!\2)[^\r])*?(?=[^\s_]_|(?=\w)\D\*\D|[^\s*]\*(?:[\W_]|$)).)(?=_(?:\W|$)|\*(?:[^*]|$))\2/g,
+ "$1$3");
+
+ return deasciify(text);
+ }
+
+
+ function _DoBlockQuotes(text) {
+
+ /*
+ text = text.replace(/
+ ( // Wrap whole match in $1
+ (
+ ^[ \t]*>[ \t]? // '>' at the start of a line
+ .+\n // rest of the first line
+ (.+\n)* // subsequent consecutive lines
+ \n* // blanks
+ )+
+ )
+ /gm, function(){...});
+ */
+
+ text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
+ function (wholeMatch, m1) {
+ var bq = m1;
+
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ // "----------bug".replace(/^-/g,"") == "bug"
+
+ bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting
+
+ // attacklab: clean up hack
+ bq = bq.replace(/~0/g, "");
+
+ bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines
+ bq = _RunBlockGamut(bq); // recurse
+
+ bq = bq.replace(/(^|\n)/g, "$1 ");
+ // These leading spaces screw with
content, so we need to fix that:
+ bq = bq.replace(
+ /(\s*
[^\r]+?<\/pre>)/gm,
+ function (wholeMatch, m1) {
+ var pre = m1;
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ pre = pre.replace(/^ /mg, "~0");
+ pre = pre.replace(/~0/g, "");
+ return pre;
+ });
+
+ return hashBlock("
\n" + bq + "\n
");
+ }
+ );
+ return text;
+ }
+
+ function _FormParagraphs(text, doNotUnhash) {
+ //
+ // Params:
+ // $text - string to process with html
tags
+ //
+
+ // Strip leading and trailing lines:
+ text = text.replace(/^\n+/g, "");
+ text = text.replace(/\n+$/g, "");
+
+ var grafs = text.split(/\n{2,}/g);
+ var grafsOut = [];
+
+ var markerRe = /~K(\d+)K/;
+
+ //
+ // Wrap
tags.
+ //
+ var end = grafs.length;
+ for (var i = 0; i < end; i++) {
+ var str = grafs[i];
+
+ // if this is an HTML marker, copy it
+ if (markerRe.test(str)) {
+ grafsOut.push(str);
+ }
+ else if (/\S/.test(str)) {
+ str = _RunSpanGamut(str);
+ str = str.replace(/^([ \t]*)/g, "
");
+ str += "
"
+ grafsOut.push(str);
+ }
+
+ }
+ //
+ // Unhashify HTML blocks
+ //
+ if (!doNotUnhash) {
+ end = grafsOut.length;
+ for (var i = 0; i < end; i++) {
+ var foundAny = true;
+ while (foundAny) { // we may need several runs, since the data may be nested
+ foundAny = false;
+ grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) {
+ foundAny = true;
+ return g_html_blocks[id];
+ });
+ }
+ }
+ }
+ return grafsOut.join("\n\n");
+ }
+
+ function _EncodeAmpsAndAngles(text) {
+ // Smart processing for ampersands and angle brackets that need to be encoded.
+
+ // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+ // http://bumppo.net/projects/amputator/
+ text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&");
+
+ // Encode naked <'s
+ text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<");
+
+ return text;
+ }
+
+ function _EncodeBackslashEscapes(text) {
+ //
+ // Parameter: String.
+ // Returns: The string, with after processing the following backslash
+ // escape sequences.
+ //
+
+ // attacklab: The polite way to do this is with the new
+ // escapeCharacters() function:
+ //
+ // text = escapeCharacters(text,"\\",true);
+ // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);
+ //
+ // ...but we're sidestepping its use of the (slow) RegExp constructor
+ // as an optimization for Firefox. This function gets called a LOT.
+
+ text = text.replace(/\\(\\)/g, escapeCharacters_callback);
+ text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback);
+ return text;
+ }
+
+ var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]",
+ charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]",
+ autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"),
+ endCharRegex = new RegExp(charEndingUrl, "i");
+
+ function handleTrailingParens(wholeMatch, lookbehind, protocol, link) {
+ if (lookbehind)
+ return wholeMatch;
+ if (link.charAt(link.length - 1) !== ")")
+ return "<" + protocol + link + ">";
+ var parens = link.match(/[()]/g);
+ var level = 0;
+ for (var i = 0; i < parens.length; i++) {
+ if (parens[i] === "(") {
+ if (level <= 0)
+ level = 1;
+ else
+ level++;
+ }
+ else {
+ level--;
+ }
+ }
+ var tail = "";
+ if (level < 0) {
+ var re = new RegExp("\\){1," + (-level) + "}$");
+ link = link.replace(re, function (trailingParens) {
+ tail = trailingParens;
+ return "";
+ });
+ }
+ if (tail) {
+ var lastChar = link.charAt(link.length - 1);
+ if (!endCharRegex.test(lastChar)) {
+ tail = lastChar + tail;
+ link = link.substr(0, link.length - 1);
+ }
+ }
+ return "<" + protocol + link + ">" + tail;
+ }
+
+ function _DoAutoLinks(text) {
+
+ // note that at this point, all other URL in the text are already hyperlinked as
+ // *except* for the case
+
+ // automatically add < and > around unadorned raw hyperlinks
+ // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character
+ // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor
+ // with a <, so there is no risk of overlapping matches.
+ text = text.replace(autoLinkRegex, handleTrailingParens);
+
+ // autolink anything like
+
+
+ var replacer = function (wholematch, m1) {
+ var url = attributeSafeUrl(m1);
+
+ return "" + pluginHooks.plainLinkText(m1) + "";
+ };
+ text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer);
+
+ // Email addresses:
+ /*
+ text = text.replace(/
+ <
+ (?:mailto:)?
+ (
+ [-.\w]+
+ \@
+ [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
+ )
+ >
+ /gi, _DoAutoLinks_callback());
+ */
+
+ /* disabling email autolinking, since we don't do that on the server, either
+ text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
+ function(wholeMatch,m1) {
+ return _EncodeEmailAddress( _UnescapeSpecialChars(m1) );
+ }
+ );
+ */
+ return text;
+ }
+
+ function _UnescapeSpecialChars(text) {
+ //
+ // Swap back in all the special characters we've hidden.
+ //
+ text = text.replace(/~E(\d+)E/g,
+ function (wholeMatch, m1) {
+ var charCodeToReplace = parseInt(m1);
+ return String.fromCharCode(charCodeToReplace);
+ }
+ );
+ return text;
+ }
+
+ function _Outdent(text) {
+ //
+ // Remove one level of line-leading tabs or spaces
+ //
+
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ // "----------bug".replace(/^-/g,"") == "bug"
+
+ text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width
+
+ // attacklab: clean up hack
+ text = text.replace(/~0/g, "")
+
+ return text;
+ }
+
+ function _Detab(text) {
+ if (!/\t/.test(text))
+ return text;
+
+ var spaces = [" ", " ", " ", " "],
+ skew = 0,
+ v;
+
+ return text.replace(/[\n\t]/g, function (match, offset) {
+ if (match === "\n") {
+ skew = offset + 1;
+ return match;
+ }
+ v = (offset - skew) % 4;
+ skew = offset + 1;
+ return spaces[v];
+ });
+ }
+
+ //
+ // attacklab: Utility functions
+ //
+
+ function attributeSafeUrl(url) {
+ url = attributeEncode(url);
+ url = escapeCharacters(url, "*_:()[]")
+ return url;
+ }
+
+ function escapeCharacters(text, charsToEscape, afterBackslash) {
+ // First we have to escape the escape characters so that
+ // we can build a character class out of them
+ var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])";
+
+ if (afterBackslash) {
+ regexString = "\\\\" + regexString;
+ }
+
+ var regex = new RegExp(regexString, "g");
+ text = text.replace(regex, escapeCharacters_callback);
+
+ return text;
+ }
+
+
+ function escapeCharacters_callback(wholeMatch, m1) {
+ var charCodeToEscape = m1.charCodeAt(0);
+ return "~E" + charCodeToEscape + "E";
+ }
+
+ }; // end of the Markdown.Converter constructor
+
+})();
\ No newline at end of file
diff --git a/assets/public/editor/Markdown.Editor.js b/assets/public/editor/Markdown.Editor.js
new file mode 100644
index 0000000..59df829
--- /dev/null
+++ b/assets/public/editor/Markdown.Editor.js
@@ -0,0 +1,2245 @@
+//modification JB: CTRL+L + CTRL+U disabled: //doClick(buttons.link); doClick(buttons.ulist);
+
+// needs Markdown.Converter.js at the moment
+
+(function () {
+
+ var util = {},
+ position = {},
+ ui = {},
+ doc = window.document,
+ re = window.RegExp,
+ nav = window.navigator,
+ SETTINGS = { lineLength: 72 },
+
+ // Used to work around some browser bugs where we can't use feature testing.
+ uaSniffed = {
+ isIE: /msie/.test(nav.userAgent.toLowerCase()),
+ isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()),
+ isOpera: /opera/.test(nav.userAgent.toLowerCase())
+ };
+
+ var defaultsStrings = {
+ bold: "Strong Ctrl+B",
+ boldexample: "strong text",
+
+ italic: "Emphasis Ctrl+I",
+ italicexample: "emphasized text",
+
+ link: "Hyperlink Ctrl+L",
+ linkdescription: "enter link description here",
+ linkdialog: "
Ctrl+H",
+ headingexample: "Heading",
+
+ hr: "Horizontal Rule Ctrl+R",
+
+ undo: "Undo - Ctrl+Z",
+ redo: "Redo - Ctrl+Y",
+ redomac: "Redo - Ctrl+Shift+Z",
+
+ help: "Markdown Editing Help"
+ };
+
+
+ // -------------------------------------------------------------------
+ // YOUR CHANGES GO HERE
+ //
+ // I've tried to localize the things you are likely to change to
+ // this area.
+ // -------------------------------------------------------------------
+
+ // The default text that appears in the dialog input box when entering
+ // links.
+ var imageDefaultText = "http://";
+ var linkDefaultText = "http://";
+
+ // -------------------------------------------------------------------
+ // END OF YOUR CHANGES
+ // -------------------------------------------------------------------
+
+ // options, if given, can have the following properties:
+ // options.helpButton = { handler: yourEventHandler }
+ // options.strings = { italicexample: "slanted text" }
+ // `yourEventHandler` is the click handler for the help button.
+ // If `options.helpButton` isn't given, not help button is created.
+ // `options.strings` can have any or all of the same properties as
+ // `defaultStrings` above, so you can just override some string displayed
+ // to the user on a case-by-case basis, or translate all strings to
+ // a different language.
+ //
+ // For backwards compatibility reasons, the `options` argument can also
+ // be just the `helpButton` object, and `strings.help` can also be set via
+ // `helpButton.title`. This should be considered legacy.
+ //
+ // The constructed editor object has the methods:
+ // - getConverter() returns the markdown converter object that was passed to the constructor
+ // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
+ // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
+ Markdown.Editor = function (markdownConverter, idPostfix, options) {
+
+ options = options || {};
+
+ if (typeof options.handler === "function") { //backwards compatible behavior
+ options = { helpButton: options };
+ }
+ options.strings = options.strings || {};
+ if (options.helpButton) {
+ options.strings.help = options.strings.help || options.helpButton.title;
+ }
+ var getString = function (identifier) { return options.strings[identifier] || defaultsStrings[identifier]; }
+
+ idPostfix = idPostfix || "";
+
+ var hooks = this.hooks = new Markdown.HookCollection();
+ hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed
+ hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
+ hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates
+ * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
+ * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
+ */
+
+ this.getConverter = function () { return markdownConverter; }
+
+ var that = this,
+ panels;
+
+ this.run = function () {
+ if (panels)
+ return; // already initialized
+
+ panels = new PanelCollection(idPostfix);
+ var commandManager = new CommandManager(hooks, getString);
+ var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });
+ var undoManager, uiManager;
+
+ if (!/\?noundo/.test(doc.location.href)) {
+ undoManager = new UndoManager(function () {
+ previewManager.refresh();
+ if (uiManager) // not available on the first call
+ uiManager.setUndoRedoButtonStates();
+ }, panels);
+ this.textOperation = function (f) {
+ undoManager.setCommandMode();
+ f();
+ that.refreshPreview();
+ }
+ }
+
+ uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString);
+ uiManager.setUndoRedoButtonStates();
+
+ var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
+
+ forceRefresh();
+ };
+
+ }
+
+ // before: contains all the text in the input box BEFORE the selection.
+ // after: contains all the text in the input box AFTER the selection.
+ function Chunks() { }
+
+ // startRegex: a regular expression to find the start tag
+ // endRegex: a regular expresssion to find the end tag
+ Chunks.prototype.findTags = function (startRegex, endRegex) {
+
+ var chunkObj = this;
+ var regex;
+
+ if (startRegex) {
+
+ regex = util.extendRegExp(startRegex, "", "$");
+
+ this.before = this.before.replace(regex,
+ function (match) {
+ chunkObj.startTag = chunkObj.startTag + match;
+ return "";
+ });
+
+ regex = util.extendRegExp(startRegex, "^", "");
+
+ this.selection = this.selection.replace(regex,
+ function (match) {
+ chunkObj.startTag = chunkObj.startTag + match;
+ return "";
+ });
+ }
+
+ if (endRegex) {
+
+ regex = util.extendRegExp(endRegex, "", "$");
+
+ this.selection = this.selection.replace(regex,
+ function (match) {
+ chunkObj.endTag = match + chunkObj.endTag;
+ return "";
+ });
+
+ regex = util.extendRegExp(endRegex, "^", "");
+
+ this.after = this.after.replace(regex,
+ function (match) {
+ chunkObj.endTag = match + chunkObj.endTag;
+ return "";
+ });
+ }
+ };
+
+ // If remove is false, the whitespace is transferred
+ // to the before/after regions.
+ //
+ // If remove is true, the whitespace disappears.
+ Chunks.prototype.trimWhitespace = function (remove) {
+ var beforeReplacer, afterReplacer, that = this;
+ if (remove) {
+ beforeReplacer = afterReplacer = "";
+ } else {
+ beforeReplacer = function (s) { that.before += s; return ""; }
+ afterReplacer = function (s) { that.after = s + that.after; return ""; }
+ }
+
+ this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
+ };
+
+
+ Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
+
+ if (nLinesBefore === undefined) {
+ nLinesBefore = 1;
+ }
+
+ if (nLinesAfter === undefined) {
+ nLinesAfter = 1;
+ }
+
+ nLinesBefore++;
+ nLinesAfter++;
+
+ var regexText;
+ var replacementText;
+
+ // chrome bug ... documented at: http://meta.stackexchange.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
+ if (navigator.userAgent.match(/Chrome/)) {
+ "X".match(/()./);
+ }
+
+ this.selection = this.selection.replace(/(^\n*)/, "");
+
+ this.startTag = this.startTag + re.$1;
+
+ this.selection = this.selection.replace(/(\n*$)/, "");
+ this.endTag = this.endTag + re.$1;
+ this.startTag = this.startTag.replace(/(^\n*)/, "");
+ this.before = this.before + re.$1;
+ this.endTag = this.endTag.replace(/(\n*$)/, "");
+ this.after = this.after + re.$1;
+
+ if (this.before) {
+
+ regexText = replacementText = "";
+
+ while (nLinesBefore--) {
+ regexText += "\\n?";
+ replacementText += "\n";
+ }
+
+ if (findExtraNewlines) {
+ regexText = "\\n*";
+ }
+ this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
+ }
+
+ if (this.after) {
+
+ regexText = replacementText = "";
+
+ while (nLinesAfter--) {
+ regexText += "\\n?";
+ replacementText += "\n";
+ }
+ if (findExtraNewlines) {
+ regexText = "\\n*";
+ }
+
+ this.after = this.after.replace(new re(regexText, ""), replacementText);
+ }
+ };
+
+ // end of Chunks
+
+ // A collection of the important regions on the page.
+ // Cached so we don't have to keep traversing the DOM.
+ // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
+ // this issue:
+ // Internet explorer has problems with CSS sprite buttons that use HTML
+ // lists. When you click on the background image "button", IE will
+ // select the non-existent link text and discard the selection in the
+ // textarea. The solution to this is to cache the textarea selection
+ // on the button's mousedown event and set a flag. In the part of the
+ // code where we need to grab the selection, we check for the flag
+ // and, if it's set, use the cached area instead of querying the
+ // textarea.
+ //
+ // This ONLY affects Internet Explorer (tested on versions 6, 7
+ // and 8) and ONLY on button clicks. Keyboard shortcuts work
+ // normally since the focus never leaves the textarea.
+ function PanelCollection(postfix) {
+ this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
+ this.preview = doc.getElementById("wmd-preview" + postfix);
+ this.input = doc.getElementById("wmd-input" + postfix);
+ };
+
+ // Returns true if the DOM element is visible, false if it's hidden.
+ // Checks if display is anything other than none.
+ util.isVisible = function (elem) {
+
+ if (window.getComputedStyle) {
+ // Most browsers
+ return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
+ }
+ else if (elem.currentStyle) {
+ // IE
+ return elem.currentStyle["display"] !== "none";
+ }
+ };
+
+
+ // Adds a listener callback to a DOM element which is fired on a specified
+ // event.
+ util.addEvent = function (elem, event, listener) {
+ if (elem.attachEvent) {
+ // IE only. The "on" is mandatory.
+ elem.attachEvent("on" + event, listener);
+ }
+ else {
+ // Other browsers.
+ elem.addEventListener(event, listener, false);
+ }
+ };
+
+
+ // Removes a listener callback from a DOM element which is fired on a specified
+ // event.
+ util.removeEvent = function (elem, event, listener) {
+ if (elem.detachEvent) {
+ // IE only. The "on" is mandatory.
+ elem.detachEvent("on" + event, listener);
+ }
+ else {
+ // Other browsers.
+ elem.removeEventListener(event, listener, false);
+ }
+ };
+
+ // Converts \r\n and \r to \n.
+ util.fixEolChars = function (text) {
+ text = text.replace(/\r\n/g, "\n");
+ text = text.replace(/\r/g, "\n");
+ return text;
+ };
+
+ // Extends a regular expression. Returns a new RegExp
+ // using pre + regex + post as the expression.
+ // Used in a few functions where we have a base
+ // expression and we want to pre- or append some
+ // conditions to it (e.g. adding "$" to the end).
+ // The flags are unchanged.
+ //
+ // regex is a RegExp, pre and post are strings.
+ util.extendRegExp = function (regex, pre, post) {
+
+ if (pre === null || pre === undefined) {
+ pre = "";
+ }
+ if (post === null || post === undefined) {
+ post = "";
+ }
+
+ var pattern = regex.toString();
+ var flags;
+
+ // Replace the flags with empty space and store them.
+ pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) {
+ flags = flagsPart;
+ return "";
+ });
+
+ // Remove the slash delimiters on the regular expression.
+ pattern = pattern.replace(/(^\/|\/$)/g, "");
+ pattern = pre + pattern + post;
+
+ return new re(pattern, flags);
+ }
+
+ // UNFINISHED
+ // The assignment in the while loop makes jslint cranky.
+ // I'll change it to a better loop later.
+ position.getTop = function (elem, isInner) {
+ var result = elem.offsetTop;
+ if (!isInner) {
+ while (elem = elem.offsetParent) {
+ result += elem.offsetTop;
+ }
+ }
+ return result;
+ };
+
+ position.getHeight = function (elem) {
+ return elem.offsetHeight || elem.scrollHeight;
+ };
+
+ position.getWidth = function (elem) {
+ return elem.offsetWidth || elem.scrollWidth;
+ };
+
+ position.getPageSize = function () {
+
+ var scrollWidth, scrollHeight;
+ var innerWidth, innerHeight;
+
+ // It's not very clear which blocks work with which browsers.
+ if (self.innerHeight && self.scrollMaxY) {
+ scrollWidth = doc.body.scrollWidth;
+ scrollHeight = self.innerHeight + self.scrollMaxY;
+ }
+ else if (doc.body.scrollHeight > doc.body.offsetHeight) {
+ scrollWidth = doc.body.scrollWidth;
+ scrollHeight = doc.body.scrollHeight;
+ }
+ else {
+ scrollWidth = doc.body.offsetWidth;
+ scrollHeight = doc.body.offsetHeight;
+ }
+
+ if (self.innerHeight) {
+ // Non-IE browser
+ innerWidth = self.innerWidth;
+ innerHeight = self.innerHeight;
+ }
+ else if (doc.documentElement && doc.documentElement.clientHeight) {
+ // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
+ innerWidth = doc.documentElement.clientWidth;
+ innerHeight = doc.documentElement.clientHeight;
+ }
+ else if (doc.body) {
+ // Other versions of IE
+ innerWidth = doc.body.clientWidth;
+ innerHeight = doc.body.clientHeight;
+ }
+
+ var maxWidth = Math.max(scrollWidth, innerWidth);
+ var maxHeight = Math.max(scrollHeight, innerHeight);
+ return [maxWidth, maxHeight, innerWidth, innerHeight];
+ };
+
+ // Handles pushing and popping TextareaStates for undo/redo commands.
+ // I should rename the stack variables to list.
+ function UndoManager(callback, panels) {
+
+ var undoObj = this;
+ var undoStack = []; // A stack of undo states
+ var stackPtr = 0; // The index of the current state
+ var mode = "none";
+ var lastState; // The last state
+ var timer; // The setTimeout handle for cancelling the timer
+ var inputStateObj;
+
+ // Set the mode for later logic steps.
+ var setMode = function (newMode, noSave) {
+ if (mode != newMode) {
+ mode = newMode;
+ if (!noSave) {
+ saveState();
+ }
+ }
+
+ if (!uaSniffed.isIE || mode != "moving") {
+ timer = setTimeout(refreshState, 1);
+ }
+ else {
+ inputStateObj = null;
+ }
+ };
+
+ var refreshState = function (isInitialState) {
+ inputStateObj = new TextareaState(panels, isInitialState);
+ timer = undefined;
+ };
+
+ this.setCommandMode = function () {
+ mode = "command";
+ saveState();
+ timer = setTimeout(refreshState, 0);
+ };
+
+ this.canUndo = function () {
+ return stackPtr > 1;
+ };
+
+ this.canRedo = function () {
+ if (undoStack[stackPtr + 1]) {
+ return true;
+ }
+ return false;
+ };
+
+ // Removes the last state and restores it.
+ this.undo = function () {
+
+ if (undoObj.canUndo()) {
+ if (lastState) {
+ // What about setting state -1 to null or checking for undefined?
+ lastState.restore();
+ lastState = null;
+ }
+ else {
+ undoStack[stackPtr] = new TextareaState(panels);
+ undoStack[--stackPtr].restore();
+
+ if (callback) {
+ callback();
+ }
+ }
+ }
+
+ mode = "none";
+ panels.input.focus();
+ refreshState();
+ };
+
+ // Redo an action.
+ this.redo = function () {
+
+ if (undoObj.canRedo()) {
+
+ undoStack[++stackPtr].restore();
+
+ if (callback) {
+ callback();
+ }
+ }
+
+ mode = "none";
+ panels.input.focus();
+ refreshState();
+ };
+
+ // Push the input area state to the stack.
+ var saveState = function () {
+ var currState = inputStateObj || new TextareaState(panels);
+
+ if (!currState) {
+ return false;
+ }
+ if (mode == "moving") {
+ if (!lastState) {
+ lastState = currState;
+ }
+ return;
+ }
+ if (lastState) {
+ if (undoStack[stackPtr - 1].text != lastState.text) {
+ undoStack[stackPtr++] = lastState;
+ }
+ lastState = null;
+ }
+ undoStack[stackPtr++] = currState;
+ undoStack[stackPtr + 1] = null;
+ if (callback) {
+ callback();
+ }
+ };
+
+ var handleCtrlYZ = function (event) {
+
+ var handled = false;
+
+ if ((event.ctrlKey || event.metaKey) && !event.altKey) {
+
+ // IE and Opera do not support charCode.
+ var keyCode = event.charCode || event.keyCode;
+ var keyCodeChar = String.fromCharCode(keyCode);
+
+ switch (keyCodeChar.toLowerCase()) {
+
+ case "y":
+ undoObj.redo();
+ handled = true;
+ break;
+
+ case "z":
+ if (!event.shiftKey) {
+ undoObj.undo();
+ }
+ else {
+ undoObj.redo();
+ }
+ handled = true;
+ break;
+ }
+ }
+
+ if (handled) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ }
+ if (window.event) {
+ window.event.returnValue = false;
+ }
+ return;
+ }
+ };
+
+ // Set the mode depending on what is going on in the input area.
+ var handleModeChange = function (event) {
+
+ if (!event.ctrlKey && !event.metaKey) {
+
+ var keyCode = event.keyCode;
+
+ if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
+ // 33 - 40: page up/dn and arrow keys
+ // 63232 - 63235: page up/dn and arrow keys on safari
+ setMode("moving");
+ }
+ else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
+ // 8: backspace
+ // 46: delete
+ // 127: delete
+ setMode("deleting");
+ }
+ else if (keyCode == 13) {
+ // 13: Enter
+ setMode("newlines");
+ }
+ else if (keyCode == 27) {
+ // 27: escape
+ setMode("escape");
+ }
+ else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
+ // 16-20 are shift, etc.
+ // 91: left window key
+ // I think this might be a little messed up since there are
+ // a lot of nonprinting keys above 20.
+ setMode("typing");
+ }
+ }
+ };
+
+ var setEventHandlers = function () {
+ util.addEvent(panels.input, "keypress", function (event) {
+ // keyCode 89: y
+ // keyCode 90: z
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && (event.keyCode == 89 || event.keyCode == 90)) {
+ event.preventDefault();
+ }
+ });
+
+ var handlePaste = function () {
+ if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {
+ if (timer == undefined) {
+ mode = "paste";
+ saveState();
+ refreshState();
+ }
+ }
+ };
+
+ util.addEvent(panels.input, "keydown", handleCtrlYZ);
+ util.addEvent(panels.input, "keydown", handleModeChange);
+ util.addEvent(panels.input, "mousedown", function () {
+ setMode("moving");
+ });
+
+ panels.input.onpaste = handlePaste;
+ panels.input.ondrop = handlePaste;
+ };
+
+ var init = function () {
+ setEventHandlers();
+ refreshState(true);
+ saveState();
+ };
+
+ init();
+ }
+
+ // end of UndoManager
+
+ // The input textarea state/contents.
+ // This is used to implement undo/redo by the undo manager.
+ function TextareaState(panels, isInitialState) {
+
+ // Aliases
+ var stateObj = this;
+ var inputArea = panels.input;
+ this.init = function () {
+ if (!util.isVisible(inputArea)) {
+ return;
+ }
+ if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box
+ return;
+ }
+
+ this.setInputAreaSelectionStartEnd();
+ this.scrollTop = inputArea.scrollTop;
+ if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
+ this.text = inputArea.value;
+ }
+
+ }
+
+ // Sets the selected text in the input box after we've performed an
+ // operation.
+ this.setInputAreaSelection = function () {
+
+ if (!util.isVisible(inputArea)) {
+ return;
+ }
+
+ if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {
+
+ inputArea.focus();
+ inputArea.selectionStart = stateObj.start;
+ inputArea.selectionEnd = stateObj.end;
+ inputArea.scrollTop = stateObj.scrollTop;
+ }
+ else if (doc.selection) {
+
+ if (doc.activeElement && doc.activeElement !== inputArea) {
+ return;
+ }
+
+ inputArea.focus();
+ var range = inputArea.createTextRange();
+ range.moveStart("character", -inputArea.value.length);
+ range.moveEnd("character", -inputArea.value.length);
+ range.moveEnd("character", stateObj.end);
+ range.moveStart("character", stateObj.start);
+ range.select();
+ }
+ };
+
+ this.setInputAreaSelectionStartEnd = function () {
+
+ if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) {
+
+ stateObj.start = inputArea.selectionStart;
+ stateObj.end = inputArea.selectionEnd;
+ }
+ else if (doc.selection) {
+
+ stateObj.text = util.fixEolChars(inputArea.value);
+
+ // IE loses the selection in the textarea when buttons are
+ // clicked. On IE we cache the selection. Here, if something is cached,
+ // we take it.
+ var range = panels.ieCachedRange || doc.selection.createRange();
+
+ var fixedRange = util.fixEolChars(range.text);
+ var marker = "\x07";
+ var markedRange = marker + fixedRange + marker;
+ range.text = markedRange;
+ var inputText = util.fixEolChars(inputArea.value);
+
+ range.moveStart("character", -markedRange.length);
+ range.text = fixedRange;
+
+ stateObj.start = inputText.indexOf(marker);
+ stateObj.end = inputText.lastIndexOf(marker) - marker.length;
+
+ var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
+
+ if (len) {
+ range.moveStart("character", -fixedRange.length);
+ while (len--) {
+ fixedRange += "\n";
+ stateObj.end += 1;
+ }
+ range.text = fixedRange;
+ }
+
+ if (panels.ieCachedRange)
+ stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange
+
+ panels.ieCachedRange = null;
+
+ this.setInputAreaSelection();
+ }
+ };
+
+ // Restore this state into the input area.
+ this.restore = function () {
+
+ if (stateObj.text != undefined && stateObj.text != inputArea.value) {
+ inputArea.value = stateObj.text;
+ }
+ this.setInputAreaSelection();
+ inputArea.scrollTop = stateObj.scrollTop;
+ };
+
+ // Gets a collection of HTML chunks from the inptut textarea.
+ this.getChunks = function () {
+
+ var chunk = new Chunks();
+ chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
+ chunk.startTag = "";
+ chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
+ chunk.endTag = "";
+ chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
+ chunk.scrollTop = stateObj.scrollTop;
+
+ return chunk;
+ };
+
+ // Sets the TextareaState properties given a chunk of markdown.
+ this.setChunks = function (chunk) {
+
+ chunk.before = chunk.before + chunk.startTag;
+ chunk.after = chunk.endTag + chunk.after;
+
+ this.start = chunk.before.length;
+ this.end = chunk.before.length + chunk.selection.length;
+ this.text = chunk.before + chunk.selection + chunk.after;
+ this.scrollTop = chunk.scrollTop;
+ };
+ this.init();
+ };
+
+ function PreviewManager(converter, panels, previewRefreshCallback) {
+
+ var managerObj = this;
+ var timeout;
+ var elapsedTime;
+ var oldInputText;
+ var maxDelay = 3000;
+ var startType = "delayed"; // The other legal value is "manual"
+
+ // Adds event listeners to elements
+ var setupEvents = function (inputElem, listener) {
+
+ util.addEvent(inputElem, "input", listener);
+ inputElem.onpaste = listener;
+ inputElem.ondrop = listener;
+
+ util.addEvent(inputElem, "keypress", listener);
+ util.addEvent(inputElem, "keydown", listener);
+ };
+
+ var getDocScrollTop = function () {
+
+ var result = 0;
+
+ if (window.innerHeight) {
+ result = window.pageYOffset;
+ }
+ else
+ if (doc.documentElement && doc.documentElement.scrollTop) {
+ result = doc.documentElement.scrollTop;
+ }
+ else
+ if (doc.body) {
+ result = doc.body.scrollTop;
+ }
+
+ return result;
+ };
+
+ var makePreviewHtml = function () {
+
+ // If there is no registered preview panel
+ // there is nothing to do.
+ if (!panels.preview)
+ return;
+
+
+ var text = panels.input.value;
+ if (text && text == oldInputText) {
+ return; // Input text hasn't changed.
+ }
+ else {
+ oldInputText = text;
+ }
+
+ var prevTime = new Date().getTime();
+
+ text = converter.makeHtml(text);
+
+ // Calculate the processing time of the HTML creation.
+ // It's used as the delay time in the event listener.
+ var currTime = new Date().getTime();
+ elapsedTime = currTime - prevTime;
+
+ pushPreviewHtml(text);
+ };
+
+ // setTimeout is already used. Used as an event listener.
+ var applyTimeout = function () {
+
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = undefined;
+ }
+
+ if (startType !== "manual") {
+
+ var delay = 0;
+
+ if (startType === "delayed") {
+ delay = elapsedTime;
+ }
+
+ if (delay > maxDelay) {
+ delay = maxDelay;
+ }
+ timeout = setTimeout(makePreviewHtml, delay);
+ }
+ };
+
+ var getScaleFactor = function (panel) {
+ if (panel.scrollHeight <= panel.clientHeight) {
+ return 1;
+ }
+ return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
+ };
+
+ var setPanelScrollTops = function () {
+ if (panels.preview) {
+ panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
+ }
+ };
+
+ this.refresh = function (requiresRefresh) {
+
+ if (requiresRefresh) {
+ oldInputText = "";
+ makePreviewHtml();
+ }
+ else {
+ applyTimeout();
+ }
+ };
+
+ this.processingTime = function () {
+ return elapsedTime;
+ };
+
+ var isFirstTimeFilled = true;
+
+ // IE doesn't let you use innerHTML if the element is contained somewhere in a table
+ // (which is the case for inline editing) -- in that case, detach the element, set the
+ // value, and reattach. Yes, that *is* ridiculous.
+ var ieSafePreviewSet = function (text) {
+ var preview = panels.preview;
+ var parent = preview.parentNode;
+ var sibling = preview.nextSibling;
+ parent.removeChild(preview);
+ preview.innerHTML = text;
+ if (!sibling)
+ parent.appendChild(preview);
+ else
+ parent.insertBefore(preview, sibling);
+ }
+
+ var nonSuckyBrowserPreviewSet = function (text) {
+ panels.preview.innerHTML = text;
+ }
+
+ var previewSetter;
+
+ var previewSet = function (text) {
+ if (previewSetter)
+ return previewSetter(text);
+
+ try {
+ nonSuckyBrowserPreviewSet(text);
+ previewSetter = nonSuckyBrowserPreviewSet;
+ } catch (e) {
+ previewSetter = ieSafePreviewSet;
+ previewSetter(text);
+ }
+ };
+
+ var pushPreviewHtml = function (text) {
+
+ var emptyTop = position.getTop(panels.input) - getDocScrollTop();
+
+ if (panels.preview) {
+ previewSet(text);
+ previewRefreshCallback();
+ }
+
+ setPanelScrollTops();
+
+ if (isFirstTimeFilled) {
+ isFirstTimeFilled = false;
+ return;
+ }
+
+ var fullTop = position.getTop(panels.input) - getDocScrollTop();
+
+ if (uaSniffed.isIE) {
+ setTimeout(function () {
+ window.scrollBy(0, fullTop - emptyTop);
+ }, 0);
+ }
+ else {
+ window.scrollBy(0, fullTop - emptyTop);
+ }
+ };
+
+ var init = function () {
+
+ setupEvents(panels.input, applyTimeout);
+ makePreviewHtml();
+
+ if (panels.preview) {
+ panels.preview.scrollTop = 0;
+ }
+ };
+
+ init();
+ };
+
+ // Creates the background behind the hyperlink text entry box.
+ // And download dialog
+ // Most of this has been moved to CSS but the div creation and
+ // browser-specific hacks remain here.
+ ui.createBackground = function () {
+
+ var background = doc.createElement("div"),
+ style = background.style;
+
+ background.className = "wmd-prompt-background";
+
+ style.position = "absolute";
+ style.top = "0";
+
+ style.zIndex = "1000";
+
+ if (uaSniffed.isIE) {
+ style.filter = "alpha(opacity=50)";
+ }
+ else {
+ style.opacity = "0.5";
+ }
+
+ var pageSize = position.getPageSize();
+ style.height = pageSize[1] + "px";
+
+ if (uaSniffed.isIE) {
+ style.left = doc.documentElement.scrollLeft;
+ style.width = doc.documentElement.clientWidth;
+ }
+ else {
+ style.left = "0";
+ style.width = "100%";
+ }
+
+ doc.body.appendChild(background);
+ return background;
+ };
+
+ // This simulates a modal dialog box and asks for the URL when you
+ // click the hyperlink or image buttons.
+ //
+ // text: The html for the input box.
+ // defaultInputText: The default value that appears in the input box.
+ // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
+ // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
+ // was chosen).
+ ui.prompt = function (text, defaultInputText, callback) {
+
+ // These variables need to be declared at this level since they are used
+ // in multiple functions.
+ var dialog; // The dialog box.
+ var input; // The text box where you enter the hyperlink.
+
+
+ if (defaultInputText === undefined) {
+ defaultInputText = "";
+ }
+
+ // Used as a keydown event handler. Esc dismisses the prompt.
+ // Key code 27 is ESC.
+ var checkEscape = function (key) {
+ var code = (key.charCode || key.keyCode);
+ if (code === 27) {
+ if (key.stopPropagation) key.stopPropagation();
+ close(true);
+ return false;
+ }
+ };
+
+ // Dismisses the hyperlink input box.
+ // isCancel is true if we don't care about the input text.
+ // isCancel is false if we are going to keep the text.
+ var close = function (isCancel) {
+ util.removeEvent(doc.body, "keyup", checkEscape);
+ var text = input.value;
+
+ if (isCancel) {
+ text = null;
+ }
+ else {
+ // Fixes common pasting errors.
+ text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
+ if (!/^(?:https?|ftp):\/\//.test(text))
+ text = 'http://' + text;
+ }
+
+ dialog.parentNode.removeChild(dialog);
+
+ callback(text);
+ return false;
+ };
+
+
+
+ // Create the text input box form/window.
+ var createDialog = function () {
+
+ // The main dialog box.
+ dialog = doc.createElement("div");
+ dialog.className = "wmd-prompt-dialog";
+ dialog.style.padding = "10px;";
+ dialog.style.position = "fixed";
+ dialog.style.width = "400px";
+ dialog.style.zIndex = "1001";
+
+ // The dialog text.
+ var question = doc.createElement("div");
+ question.innerHTML = text;
+ question.style.padding = "5px";
+ dialog.appendChild(question);
+
+ // The web form container for the text box and buttons.
+ var form = doc.createElement("form"),
+ style = form.style;
+ form.onsubmit = function () { return close(false); };
+ style.padding = "0";
+ style.margin = "0";
+ style.cssFloat = "left";
+ style.width = "100%";
+ style.textAlign = "center";
+ style.position = "relative";
+ dialog.appendChild(form);
+
+ // The input text box
+ input = doc.createElement("input");
+ input.type = "text";
+ input.value = defaultInputText;
+ style = input.style;
+ style.display = "block";
+ style.width = "80%";
+ style.marginLeft = style.marginRight = "auto";
+ form.appendChild(input);
+
+ // The ok button
+ var okButton = doc.createElement("input");
+ okButton.type = "button";
+ okButton.onclick = function () { return close(false); };
+ okButton.value = "OK";
+ style = okButton.style;
+ style.margin = "10px";
+ style.display = "inline";
+ style.width = "7em";
+
+
+ // The cancel button
+ var cancelButton = doc.createElement("input");
+ cancelButton.type = "button";
+ cancelButton.onclick = function () { return close(true); };
+ cancelButton.value = "Cancel";
+ style = cancelButton.style;
+ style.margin = "10px";
+ style.display = "inline";
+ style.width = "7em";
+
+ form.appendChild(okButton);
+ form.appendChild(cancelButton);
+
+ util.addEvent(doc.body, "keyup", checkEscape);
+ dialog.style.top = "50%";
+ dialog.style.left = "50%";
+ dialog.style.display = "block";
+ if (uaSniffed.isIE_5or6) {
+ dialog.style.position = "absolute";
+ dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
+ dialog.style.left = "50%";
+ }
+ doc.body.appendChild(dialog);
+
+ // This has to be done AFTER adding the dialog to the form if you
+ // want it to be centered.
+ dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
+ dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
+
+ };
+
+ // Why is this in a zero-length timeout?
+ // Is it working around a browser bug?
+ setTimeout(function () {
+
+ createDialog();
+
+ var defTextLen = defaultInputText.length;
+ if (input.selectionStart !== undefined) {
+ input.selectionStart = 0;
+ input.selectionEnd = defTextLen;
+ }
+ else if (input.createTextRange) {
+ var range = input.createTextRange();
+ range.collapse(false);
+ range.moveStart("character", -defTextLen);
+ range.moveEnd("character", defTextLen);
+ range.select();
+ }
+
+ input.focus();
+ }, 0);
+ };
+
+ function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) {
+
+ var inputBox = panels.input,
+ buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
+
+ makeSpritedButtonRow();
+
+ var keyEvent = "keydown";
+ if (uaSniffed.isOpera) {
+ keyEvent = "keypress";
+ }
+
+ util.addEvent(inputBox, keyEvent, function (key) {
+
+ // Check to see if we have a button key and, if so execute the callback.
+ if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
+
+ var keyCode = key.charCode || key.keyCode;
+ var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
+
+ switch (keyCodeStr) {
+ case "b":
+ doClick(buttons.bold);
+ break;
+ case "i":
+ doClick(buttons.italic);
+ break;
+ case "l":
+ //doClick(buttons.link);
+ break;
+ case "q":
+ doClick(buttons.quote);
+ break;
+ case "k":
+ doClick(buttons.code);
+ break;
+ case "g":
+ doClick(buttons.image);
+ break;
+ case "o":
+ doClick(buttons.olist);
+ break;
+ case "u":
+ //doClick(buttons.ulist);
+ break;
+ case "h":
+ doClick(buttons.heading);
+ break;
+ case "r":
+ doClick(buttons.hr);
+ break;
+ case "y":
+ doClick(buttons.redo);
+ break;
+ case "z":
+ if (key.shiftKey) {
+ doClick(buttons.redo);
+ }
+ else {
+ doClick(buttons.undo);
+ }
+ break;
+ default:
+ return;
+ }
+
+
+ if (key.preventDefault) {
+ key.preventDefault();
+ }
+
+ if (window.event) {
+ window.event.returnValue = false;
+ }
+ }
+ });
+
+ // Auto-indent on shift-enter
+ util.addEvent(inputBox, "keyup", function (key) {
+ if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
+ var keyCode = key.charCode || key.keyCode;
+ // Character 13 is Enter
+ if (keyCode === 13) {
+ var fakeButton = {};
+ fakeButton.textOp = bindCommand("doAutoindent");
+ doClick(fakeButton);
+ }
+ }
+ });
+
+ // special handler because IE clears the context of the textbox on ESC
+ if (uaSniffed.isIE) {
+ util.addEvent(inputBox, "keydown", function (key) {
+ var code = key.keyCode;
+ if (code === 27) {
+ return false;
+ }
+ });
+ }
+
+
+ // Perform the button's action.
+ function doClick(button) {
+
+ inputBox.focus();
+
+ if (button.textOp) {
+
+ if (undoManager) {
+ undoManager.setCommandMode();
+ }
+
+ var state = new TextareaState(panels);
+
+ if (!state) {
+ return;
+ }
+
+ var chunks = state.getChunks();
+
+ // Some commands launch a "modal" prompt dialog. Javascript
+ // can't really make a modal dialog box and the WMD code
+ // will continue to execute while the dialog is displayed.
+ // This prevents the dialog pattern I'm used to and means
+ // I can't do something like this:
+ //
+ // var link = CreateLinkDialog();
+ // makeMarkdownLink(link);
+ //
+ // Instead of this straightforward method of handling a
+ // dialog I have to pass any code which would execute
+ // after the dialog is dismissed (e.g. link creation)
+ // in a function parameter.
+ //
+ // Yes this is awkward and I think it sucks, but there's
+ // no real workaround. Only the image and link code
+ // create dialogs and require the function pointers.
+ var fixupInputArea = function () {
+
+ inputBox.focus();
+
+ if (chunks) {
+ state.setChunks(chunks);
+ }
+
+ state.restore();
+ previewManager.refresh();
+ };
+
+ var noCleanup = button.textOp(chunks, fixupInputArea);
+
+ if (!noCleanup) {
+ fixupInputArea();
+ }
+
+ }
+
+ if (button.execute) {
+ button.execute(undoManager);
+ }
+ };
+
+ function setupButton(button, isEnabled) {
+
+ var normalYShift = "0px";
+ var disabledYShift = "-20px";
+ var highlightYShift = "-40px";
+ var image = button.getElementsByTagName("span")[0];
+ if (isEnabled) {
+ image.style.backgroundPosition = button.XShift + " " + normalYShift;
+ button.onmouseover = function () {
+ image.style.backgroundPosition = this.XShift + " " + highlightYShift;
+ };
+
+ button.onmouseout = function () {
+ image.style.backgroundPosition = this.XShift + " " + normalYShift;
+ };
+
+ // IE tries to select the background image "button" text (it's
+ // implemented in a list item) so we have to cache the selection
+ // on mousedown.
+ if (uaSniffed.isIE) {
+ button.onmousedown = function () {
+ if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection
+ return;
+ }
+ panels.ieCachedRange = document.selection.createRange();
+ panels.ieCachedScrollTop = panels.input.scrollTop;
+ };
+ }
+
+ if (!button.isHelp) {
+ button.onclick = function () {
+ if (this.onmouseout) {
+ this.onmouseout();
+ }
+ doClick(this);
+ return false;
+ }
+ }
+ }
+ else {
+ image.style.backgroundPosition = button.XShift + " " + disabledYShift;
+ button.onmouseover = button.onmouseout = button.onclick = function () { };
+ }
+ }
+
+ function bindCommand(method) {
+ if (typeof method === "string")
+ method = commandManager[method];
+ return function () { method.apply(commandManager, arguments); }
+ }
+
+ function makeSpritedButtonRow() {
+
+ var buttonBar = panels.buttonBar;
+
+ var normalYShift = "0px";
+ var disabledYShift = "-20px";
+ var highlightYShift = "-40px";
+
+ var buttonRow = document.createElement("ul");
+ buttonRow.id = "wmd-button-row" + postfix;
+ buttonRow.className = 'wmd-button-row';
+ buttonRow = buttonBar.appendChild(buttonRow);
+ var xPosition = 0;
+ var makeButton = function (id, title, XShift, textOp) {
+ var button = document.createElement("li");
+ button.className = "wmd-button";
+ button.style.left = xPosition + "px";
+ xPosition += 25;
+ var buttonImage = document.createElement("span");
+ button.id = id + postfix;
+ button.appendChild(buttonImage);
+ button.title = title;
+ button.XShift = XShift;
+ if (textOp)
+ button.textOp = textOp;
+ setupButton(button, true);
+ buttonRow.appendChild(button);
+ return button;
+ };
+ var makeSpacer = function (num) {
+ var spacer = document.createElement("li");
+ spacer.className = "wmd-spacer wmd-spacer" + num;
+ spacer.id = "wmd-spacer" + num + postfix;
+ buttonRow.appendChild(spacer);
+ xPosition += 25;
+ }
+
+ buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold"));
+ buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic"));
+ makeSpacer(1);
+ buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function (chunk, postProcessing) {
+ return this.doLinkOrImage(chunk, postProcessing, false);
+ }));
+ buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote"));
+ buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode"));
+ buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function (chunk, postProcessing) {
+ return this.doLinkOrImage(chunk, postProcessing, true);
+ }));
+ makeSpacer(2);
+ buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function (chunk, postProcessing) {
+ this.doList(chunk, postProcessing, true);
+ }));
+ buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function (chunk, postProcessing) {
+ this.doList(chunk, postProcessing, false);
+ }));
+ buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading"));
+ buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule"));
+ makeSpacer(3);
+ buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null);
+ buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
+
+ var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
+ getString("redo") :
+ getString("redomac"); // mac and other non-Windows platforms
+
+ buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);
+ buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
+
+ if (helpOptions) {
+ var helpButton = document.createElement("li");
+ var helpButtonImage = document.createElement("span");
+ helpButton.appendChild(helpButtonImage);
+ helpButton.className = "wmd-button wmd-help-button";
+ helpButton.id = "wmd-help-button" + postfix;
+ helpButton.XShift = "-240px";
+ helpButton.isHelp = true;
+ helpButton.style.right = "0px";
+ helpButton.title = getString("help");
+ helpButton.onclick = helpOptions.handler;
+
+ setupButton(helpButton, true);
+ buttonRow.appendChild(helpButton);
+ buttons.help = helpButton;
+ }
+
+ setUndoRedoButtonStates();
+ }
+
+ function setUndoRedoButtonStates() {
+ if (undoManager) {
+ setupButton(buttons.undo, undoManager.canUndo());
+ setupButton(buttons.redo, undoManager.canRedo());
+ }
+ };
+
+ this.setUndoRedoButtonStates = setUndoRedoButtonStates;
+
+ }
+
+ function CommandManager(pluginHooks, getString) {
+ this.hooks = pluginHooks;
+ this.getString = getString;
+ }
+
+ var commandProto = CommandManager.prototype;
+
+ // The markdown symbols - 4 spaces = code, > = blockquote, etc.
+ commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
+
+ // Remove markdown symbols from the chunk selection.
+ commandProto.unwrap = function (chunk) {
+ var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
+ chunk.selection = chunk.selection.replace(txt, "$1 $2");
+ };
+
+ commandProto.wrap = function (chunk, len) {
+ this.unwrap(chunk);
+ var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"),
+ that = this;
+
+ chunk.selection = chunk.selection.replace(regex, function (line, marked) {
+ if (new re("^" + that.prefixes, "").test(line)) {
+ return line;
+ }
+ return marked + "\n";
+ });
+
+ chunk.selection = chunk.selection.replace(/\s+$/, "");
+ };
+
+ commandProto.doBold = function (chunk, postProcessing) {
+ return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample"));
+ };
+
+ commandProto.doItalic = function (chunk, postProcessing) {
+ return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample"));
+ };
+
+ // chunk: The selected region that will be enclosed with */**
+ // nStars: 1 for italics, 2 for bold
+ // insertText: If you just click the button without highlighting text, this gets inserted
+ commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
+
+ // Get rid of whitespace and fixup newlines.
+ chunk.trimWhitespace();
+ chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
+
+ // Look for stars before and after. Is the chunk already marked up?
+ // note that these regex matches cannot fail
+ var starsBefore = /(\**$)/.exec(chunk.before)[0];
+ var starsAfter = /(^\**)/.exec(chunk.after)[0];
+
+ var prevStars = Math.min(starsBefore.length, starsAfter.length);
+
+ // Remove stars if we have to since the button acts as a toggle.
+ if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
+ chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
+ chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
+ }
+ else if (!chunk.selection && starsAfter) {
+ // It's not really clear why this code is necessary. It just moves
+ // some arbitrary stuff around.
+ chunk.after = chunk.after.replace(/^([*_]*)/, "");
+ chunk.before = chunk.before.replace(/(\s?)$/, "");
+ var whitespace = re.$1;
+ chunk.before = chunk.before + starsAfter + whitespace;
+ }
+ else {
+
+ // In most cases, if you don't have any selected text and click the button
+ // you'll get a selected, marked up region with the default text inserted.
+ if (!chunk.selection && !starsAfter) {
+ chunk.selection = insertText;
+ }
+
+ // Add the true markup.
+ var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
+ chunk.before = chunk.before + markup;
+ chunk.after = markup + chunk.after;
+ }
+
+ return;
+ };
+
+ commandProto.stripLinkDefs = function (text, defsToAdd) {
+
+ text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
+ function (totalMatch, id, link, newlines, title) {
+ defsToAdd[id] = totalMatch.replace(/\s*$/, "");
+ if (newlines) {
+ // Strip the title and return that separately.
+ defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
+ return newlines + title;
+ }
+ return "";
+ });
+
+ return text;
+ };
+
+ commandProto.addLinkDef = function (chunk, linkDef) {
+
+ var refNumber = 0; // The current reference number
+ var defsToAdd = {}; //
+ // Start with a clean slate by removing all previous link definitions.
+ chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
+ chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
+ chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
+
+ var defs = "";
+ var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
+
+ var addDefNumber = function (def) {
+ refNumber++;
+ def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:");
+ defs += "\n" + def;
+ };
+
+ // note that
+ // a) the recursive call to getLink cannot go infinite, because by definition
+ // of regex, inner is always a proper substring of wholeMatch, and
+ // b) more than one level of nesting is neither supported by the regex
+ // nor making a lot of sense (the only use case for nesting is a linked image)
+ var getLink = function (wholeMatch, before, inner, afterInner, id, end) {
+ inner = inner.replace(regex, getLink);
+ if (defsToAdd[id]) {
+ addDefNumber(defsToAdd[id]);
+ return before + inner + afterInner + refNumber + end;
+ }
+ return wholeMatch;
+ };
+
+ chunk.before = chunk.before.replace(regex, getLink);
+
+ if (linkDef) {
+ addDefNumber(linkDef);
+ }
+ else {
+ chunk.selection = chunk.selection.replace(regex, getLink);
+ }
+
+ var refOut = refNumber;
+
+ chunk.after = chunk.after.replace(regex, getLink);
+
+ if (chunk.after) {
+ chunk.after = chunk.after.replace(/\n*$/, "");
+ }
+ if (!chunk.after) {
+ chunk.selection = chunk.selection.replace(/\n*$/, "");
+ }
+
+ chunk.after += "\n\n" + defs;
+
+ return refOut;
+ };
+
+ // takes the line as entered into the add link/as image dialog and makes
+ // sure the URL and the optinal title are "nice".
+ function properlyEncoded(linkdef) {
+ return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
+
+ var inQueryString = false;
+
+ // Having `[^\w\d-./]` in there is just a shortcut that lets us skip
+ // the most common characters in URLs. Replacing that it with `.` would not change
+ // the result, because encodeURI returns those characters unchanged, but it
+ // would mean lots of unnecessary replacement calls. Having `[` and `]` in that
+ // section as well means we do *not* enocde square brackets. These characters are
+ // a strange beast in URLs, but if anything, this causes URLs to be more readable,
+ // and we leave it to the browser to make sure that these links are handled without
+ // problems.
+ link = link.replace(/%(?:[\da-fA-F]{2})|\?|\+|[^\w\d-./[\]]/g, function (match) {
+ // Valid percent encoding. Could just return it as is, but we follow RFC3986
+ // Section 2.1 which says "For consistency, URI producers and normalizers
+ // should use uppercase hexadecimal digits for all percent-encodings."
+ // Note that we also handle (illegal) stand-alone percent characters by
+ // replacing them with "%25"
+ if (match.length === 3 && match.charAt(0) == "%") {
+ return match.toUpperCase();
+ }
+ switch (match) {
+ case "?":
+ inQueryString = true;
+ return "?";
+ break;
+
+ // In the query string, a plus and a space are identical -- normalize.
+ // Not strictly necessary, but identical behavior to the previous version
+ // of this function.
+ case "+":
+ if (inQueryString)
+ return "%20";
+ break;
+ }
+ return encodeURI(match);
+ })
+
+ if (title) {
+ title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
+ title = title.replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">");
+ }
+ return title ? link + ' "' + title + '"' : link;
+ });
+ }
+
+ commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
+
+ chunk.trimWhitespace();
+ chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
+ var background;
+
+ if (chunk.endTag.length > 1 && chunk.startTag.length > 0) {
+
+ chunk.startTag = chunk.startTag.replace(/!?\[/, "");
+ chunk.endTag = "";
+ this.addLinkDef(chunk, null);
+
+ }
+ else {
+
+ // We're moving start and end tag back into the selection, since (as we're in the else block) we're not
+ // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
+ // link text. linkEnteredCallback takes care of escaping any brackets.
+ chunk.selection = chunk.startTag + chunk.selection + chunk.endTag;
+ chunk.startTag = chunk.endTag = "";
+
+ if (/\n\n/.test(chunk.selection)) {
+ this.addLinkDef(chunk, null);
+ return;
+ }
+ var that = this;
+ // The function to be executed when you enter a link and press OK or Cancel.
+ // Marks up the link and adds the ref.
+ var linkEnteredCallback = function (link) {
+
+ background.parentNode.removeChild(background);
+
+ if (link !== null) {
+ // ( $1
+ // [^\\] anything that's not a backslash
+ // (?:\\\\)* an even number (this includes zero) of backslashes
+ // )
+ // (?= followed by
+ // [[\]] an opening or closing bracket
+ // )
+ //
+ // In other words, a non-escaped bracket. These have to be escaped now to make sure they
+ // don't count as the end of the link or similar.
+ // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets),
+ // the bracket in one match may be the "not a backslash" character in the next match, so it
+ // should not be consumed by the first match.
+ // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the
+ // start of the string, so this also works if the selection begins with a bracket. We cannot solve
+ // this by anchoring with ^, because in the case that the selection starts with two brackets, this
+ // would mean a zero-width match at the start. Since zero-width matches advance the string position,
+ // the first bracket could then not act as the "not a backslash" for the second.
+ chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
+
+ var linkDef = " [999]: " + properlyEncoded(link);
+
+ var num = that.addLinkDef(chunk, linkDef);
+ chunk.startTag = isImage ? "![" : "[";
+ chunk.endTag = "][" + num + "]";
+
+ if (!chunk.selection) {
+ if (isImage) {
+ chunk.selection = that.getString("imagedescription");
+ }
+ else {
+ chunk.selection = that.getString("linkdescription");
+ }
+ }
+ }
+ postProcessing();
+ };
+
+ background = ui.createBackground();
+
+ if (isImage) {
+ if (!this.hooks.insertImageDialog(linkEnteredCallback))
+ ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback);
+ }
+ else {
+ ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback);
+ }
+ return true;
+ }
+ };
+
+ // When making a list, hitting shift-enter will put your cursor on the next line
+ // at the current indent level.
+ commandProto.doAutoindent = function (chunk, postProcessing) {
+
+ var commandMgr = this,
+ fakeSelection = false;
+
+ chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
+ chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
+ chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
+
+ // There's no selection, end the cursor wasn't at the end of the line:
+ // The user wants to split the current list item / code line / blockquote line
+ // (for the latter it doesn't really matter) in two. Temporarily select the
+ // (rest of the) line to achieve this.
+ if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
+ chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
+ chunk.selection = wholeMatch;
+ return "";
+ });
+ fakeSelection = true;
+ }
+
+ if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
+ if (commandMgr.doList) {
+ commandMgr.doList(chunk);
+ }
+ }
+ if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
+ if (commandMgr.doBlockquote) {
+ commandMgr.doBlockquote(chunk);
+ }
+ }
+ if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
+ if (commandMgr.doCode) {
+ commandMgr.doCode(chunk);
+ }
+ }
+
+ if (fakeSelection) {
+ chunk.after = chunk.selection + chunk.after;
+ chunk.selection = "";
+ }
+ };
+
+ commandProto.doBlockquote = function (chunk, postProcessing) {
+
+ chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
+ function (totalMatch, newlinesBefore, text, newlinesAfter) {
+ chunk.before += newlinesBefore;
+ chunk.after = newlinesAfter + chunk.after;
+ return text;
+ });
+
+ chunk.before = chunk.before.replace(/(>[ \t]*)$/,
+ function (totalMatch, blankLine) {
+ chunk.selection = blankLine + chunk.selection;
+ return "";
+ });
+
+ chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
+ chunk.selection = chunk.selection || this.getString("quoteexample");
+
+ // The original code uses a regular expression to find out how much of the
+ // text *directly before* the selection already was a blockquote:
+
+ /*
+ if (chunk.before) {
+ chunk.before = chunk.before.replace(/\n?$/, "\n");
+ }
+ chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
+ function (totalMatch) {
+ chunk.startTag = totalMatch;
+ return "";
+ });
+ */
+
+ // This comes down to:
+ // Go backwards as many lines a possible, such that each line
+ // a) starts with ">", or
+ // b) is almost empty, except for whitespace, or
+ // c) is preceeded by an unbroken chain of non-empty lines
+ // leading up to a line that starts with ">" and at least one more character
+ // and in addition
+ // d) at least one line fulfills a)
+ //
+ // Since this is essentially a backwards-moving regex, it's susceptible to
+ // catstrophic backtracking and can cause the browser to hang;
+ // see e.g. http://meta.stackexchange.com/questions/9807.
+ //
+ // Hence we replaced this by a simple state machine that just goes through the
+ // lines and checks for a), b), and c).
+
+ var match = "",
+ leftOver = "",
+ line;
+ if (chunk.before) {
+ var lines = chunk.before.replace(/\n$/, "").split("\n");
+ var inChain = false;
+ for (var i = 0; i < lines.length; i++) {
+ var good = false;
+ line = lines[i];
+ inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
+ if (/^>/.test(line)) { // a)
+ good = true;
+ if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain
+ inChain = true;
+ } else if (/^[ \t]*$/.test(line)) { // b)
+ good = true;
+ } else {
+ 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
+ }
+ if (good) {
+ match += line + "\n";
+ } else {
+ leftOver += match + line;
+ match = "\n";
+ }
+ }
+ if (!/(^|\n)>/.test(match)) { // d)
+ leftOver += match;
+ match = "";
+ }
+ }
+
+ chunk.startTag = match;
+ chunk.before = leftOver;
+
+ // end of change
+
+ if (chunk.after) {
+ chunk.after = chunk.after.replace(/^\n?/, "\n");
+ }
+
+ chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
+ function (totalMatch) {
+ chunk.endTag = totalMatch;
+ return "";
+ }
+ );
+
+ var replaceBlanksInTags = function (useBracket) {
+
+ var replacement = useBracket ? "> " : "";
+
+ if (chunk.startTag) {
+ chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
+ function (totalMatch, markdown) {
+ return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
+ });
+ }
+ if (chunk.endTag) {
+ chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
+ function (totalMatch, markdown) {
+ return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
+ });
+ }
+ };
+
+ if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
+ this.wrap(chunk, SETTINGS.lineLength - 2);
+ chunk.selection = chunk.selection.replace(/^/gm, "> ");
+ replaceBlanksInTags(true);
+ chunk.skipLines();
+ } else {
+ chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
+ this.unwrap(chunk);
+ replaceBlanksInTags(false);
+
+ if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
+ chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
+ }
+
+ if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
+ chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
+ }
+ }
+
+ chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
+
+ if (!/\n/.test(chunk.selection)) {
+ chunk.selection = chunk.selection.replace(/^(> *)/,
+ function (wholeMatch, blanks) {
+ chunk.startTag += blanks;
+ return "";
+ });
+ }
+ };
+
+ commandProto.doCode = function (chunk, postProcessing) {
+
+ var hasTextBefore = /\S[ ]*$/.test(chunk.before);
+ var hasTextAfter = /^[ ]*\S/.test(chunk.after);
+
+ // Use 'four space' markdown if the selection is on its own
+ // line or is multiline.
+ if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
+
+ chunk.before = chunk.before.replace(/[ ]{4}$/,
+ function (totalMatch) {
+ chunk.selection = totalMatch + chunk.selection;
+ return "";
+ });
+
+ var nLinesBack = 1;
+ var nLinesForward = 1;
+
+ if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
+ nLinesBack = 0;
+ }
+ if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
+ nLinesForward = 0;
+ }
+
+ chunk.skipLines(nLinesBack, nLinesForward);
+
+ if (!chunk.selection) {
+ chunk.startTag = " ";
+ chunk.selection = this.getString("codeexample");
+ }
+ else {
+ if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
+ if (/\n/.test(chunk.selection))
+ chunk.selection = chunk.selection.replace(/^/gm, " ");
+ else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
+ chunk.before += " ";
+ }
+ else {
+ chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, "");
+ }
+ }
+ }
+ else {
+ // Use backticks (`) to delimit the code block.
+
+ chunk.trimWhitespace();
+ chunk.findTags(/`/, /`/);
+
+ if (!chunk.startTag && !chunk.endTag) {
+ chunk.startTag = chunk.endTag = "`";
+ if (!chunk.selection) {
+ chunk.selection = this.getString("codeexample");
+ }
+ }
+ else if (chunk.endTag && !chunk.startTag) {
+ chunk.before += chunk.endTag;
+ chunk.endTag = "";
+ }
+ else {
+ chunk.startTag = chunk.endTag = "";
+ }
+ }
+ };
+
+ commandProto.doList = function (chunk, postProcessing, isNumberedList) {
+
+ // These are identical except at the very beginning and end.
+ // Should probably use the regex extension function to make this clearer.
+ var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
+ var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
+
+ // The default bullet is a dash but others are possible.
+ // This has nothing to do with the particular HTML bullet,
+ // it's just a markdown bullet.
+ var bullet = "-";
+
+ // The number in a numbered list.
+ var num = 1;
+
+ // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
+ var getItemPrefix = function () {
+ var prefix;
+ if (isNumberedList) {
+ prefix = " " + num + ". ";
+ num++;
+ }
+ else {
+ prefix = " " + bullet + " ";
+ }
+ return prefix;
+ };
+
+ // Fixes the prefixes of the other list items.
+ var getPrefixedItem = function (itemText) {
+
+ // The numbering flag is unset when called by autoindent.
+ if (isNumberedList === undefined) {
+ isNumberedList = /^\s*\d/.test(itemText);
+ }
+
+ // Renumber/bullet the list element.
+ itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
+ function (_) {
+ return getItemPrefix();
+ });
+
+ return itemText;
+ };
+
+ chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
+
+ if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
+ chunk.before += chunk.startTag;
+ chunk.startTag = "";
+ }
+
+ if (chunk.startTag) {
+
+ var hasDigits = /\d+[.]/.test(chunk.startTag);
+ chunk.startTag = "";
+ chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
+ this.unwrap(chunk);
+ chunk.skipLines();
+
+ if (hasDigits) {
+ // Have to renumber the bullet points if this is a numbered list.
+ chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
+ }
+ if (isNumberedList == hasDigits) {
+ return;
+ }
+ }
+
+ var nLinesUp = 1;
+
+ chunk.before = chunk.before.replace(previousItemsRegex,
+ function (itemText) {
+ if (/^\s*([*+-])/.test(itemText)) {
+ bullet = re.$1;
+ }
+ nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
+ return getPrefixedItem(itemText);
+ });
+
+ if (!chunk.selection) {
+ chunk.selection = this.getString("litem");
+ }
+
+ var prefix = getItemPrefix();
+
+ var nLinesDown = 1;
+
+ chunk.after = chunk.after.replace(nextItemsRegex,
+ function (itemText) {
+ nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
+ return getPrefixedItem(itemText);
+ });
+
+ chunk.trimWhitespace(true);
+ chunk.skipLines(nLinesUp, nLinesDown, true);
+ chunk.startTag = prefix;
+ var spaces = prefix.replace(/./g, " ");
+ this.wrap(chunk, SETTINGS.lineLength - spaces.length);
+ chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
+
+ };
+
+ commandProto.doHeading = function (chunk, postProcessing) {
+
+ // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
+ chunk.selection = chunk.selection.replace(/\s+/g, " ");
+ chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
+
+ // If we clicked the button with no selected text, we just
+ // make a level 2 hash header around some default text.
+ if (!chunk.selection) {
+ chunk.startTag = "## ";
+ chunk.selection = this.getString("headingexample");
+ chunk.endTag = " ##";
+ return;
+ }
+
+ var headerLevel = 0; // The existing header level of the selected text.
+
+ // Remove any existing hash heading markdown and save the header level.
+ chunk.findTags(/#+[ ]*/, /[ ]*#+/);
+ if (/#+/.test(chunk.startTag)) {
+ headerLevel = re.lastMatch.length;
+ }
+ chunk.startTag = chunk.endTag = "";
+
+ // Try to get the current header level by looking for - and = in the line
+ // below the selection.
+ chunk.findTags(null, /\s?(-+|=+)/);
+ if (/=+/.test(chunk.endTag)) {
+ headerLevel = 1;
+ }
+ if (/-+/.test(chunk.endTag)) {
+ headerLevel = 2;
+ }
+
+ // Skip to the next line so we can create the header markdown.
+ chunk.startTag = chunk.endTag = "";
+ chunk.skipLines(1, 1);
+
+ // We make a level 2 header if there is no current header.
+ // If there is a header level, we substract one from the header level.
+ // If it's already a level 1 header, it's removed.
+ var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
+
+ if (headerLevelToCreate > 0) {
+
+ // The button only creates level 1 and 2 underline headers.
+ // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner?
+ var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
+ var len = chunk.selection.length;
+ if (len > SETTINGS.lineLength) {
+ len = SETTINGS.lineLength;
+ }
+ chunk.endTag = "\n";
+ while (len--) {
+ chunk.endTag += headerChar;
+ }
+ }
+ };
+
+ commandProto.doHorizontalRule = function (chunk, postProcessing) {
+ chunk.startTag = "----------\n";
+ chunk.selection = "";
+ chunk.skipLines(2, 1, true);
+ }
+
+
+})();
\ No newline at end of file
diff --git a/assets/public/editor/Markdown.Extra.js b/assets/public/editor/Markdown.Extra.js
new file mode 100644
index 0000000..d3b859a
--- /dev/null
+++ b/assets/public/editor/Markdown.Extra.js
@@ -0,0 +1,874 @@
+(function () {
+ // A quick way to make sure we're only keeping span-level tags when we need to.
+ // This isn't supposed to be foolproof. It's just a quick way to make sure we
+ // keep all span-level tags returned by a pagedown converter. It should allow
+ // all span-level tags through, with or without attributes.
+ var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|',
+ 'bdo|big|button|cite|code|del|dfn|em|figcaption|',
+ 'font|i|iframe|img|input|ins|kbd|label|map|',
+ 'mark|meter|object|param|progress|q|ruby|rp|rt|s|',
+ 'samp|script|select|small|span|strike|strong|',
+ 'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|',
+ '<(br)\\s?\\/?>)$'].join(''), 'i');
+
+ /******************************************************************
+ * Utility Functions *
+ *****************************************************************/
+
+ // patch for ie7
+ if (!Array.indexOf) {
+ Array.prototype.indexOf = function(obj) {
+ for (var i = 0; i < this.length; i++) {
+ if (this[i] == obj) {
+ return i;
+ }
+ }
+ return -1;
+ };
+ }
+
+ function trim(str) {
+ return str.replace(/^\s+|\s+$/g, '');
+ }
+
+ function rtrim(str) {
+ return str.replace(/\s+$/g, '');
+ }
+
+ // Remove one level of indentation from text. Indent is 4 spaces.
+ function outdent(text) {
+ return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), '');
+ }
+
+ function contains(str, substr) {
+ return str.indexOf(substr) != -1;
+ }
+
+ // Sanitize html, removing tags that aren't in the whitelist
+ function sanitizeHtml(html, whitelist) {
+ return html.replace(/<[^>]*>?/gi, function(tag) {
+ return tag.match(whitelist) ? tag : '';
+ });
+ }
+
+ // Merge two arrays, keeping only unique elements.
+ function union(x, y) {
+ var obj = {};
+ for (var i = 0; i < x.length; i++)
+ obj[x[i]] = x[i];
+ for (i = 0; i < y.length; i++)
+ obj[y[i]] = y[i];
+ var res = [];
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k))
+ res.push(obj[k]);
+ }
+ return res;
+ }
+
+ // JS regexes don't support \A or \Z, so we add sentinels, as Pagedown
+ // does. In this case, we add the ascii codes for start of text (STX) and
+ // end of text (ETX), an idea borrowed from:
+ // https://github.com/tanakahisateru/js-markdown-extra
+ function addAnchors(text) {
+ if(text.charAt(0) != '\x02')
+ text = '\x02' + text;
+ if(text.charAt(text.length - 1) != '\x03')
+ text = text + '\x03';
+ return text;
+ }
+
+ // Remove STX and ETX sentinels.
+ function removeAnchors(text) {
+ if(text.charAt(0) == '\x02')
+ text = text.substr(1);
+ if(text.charAt(text.length - 1) == '\x03')
+ text = text.substr(0, text.length - 1);
+ return text;
+ }
+
+ // Convert markdown within an element, retaining only span-level tags
+ function convertSpans(text, extra) {
+ return sanitizeHtml(convertAll(text, extra), inlineTags);
+ }
+
+ // Convert internal markdown using the stock pagedown converter
+ function convertAll(text, extra) {
+ var result = extra.blockGamutHookCallback(text);
+ // We need to perform these operations since we skip the steps in the converter
+ result = unescapeSpecialChars(result);
+ result = result.replace(/~D/g, "$$").replace(/~T/g, "~");
+ result = extra.previousPostConversion(result);
+ return result;
+ }
+
+ // Convert escaped special characters
+ function processEscapesStep1(text) {
+ // Markdown extra adds two escapable characters, `:` and `|`
+ return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i');
+ }
+ function processEscapesStep2(text) {
+ return text.replace(/~I/g, '|').replace(/~i/g, ':');
+ }
+
+ // Duplicated from PageDown converter
+ function unescapeSpecialChars(text) {
+ // Swap back in all the special characters we've hidden.
+ text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) {
+ var charCodeToReplace = parseInt(m1);
+ return String.fromCharCode(charCodeToReplace);
+ });
+ return text;
+ }
+
+ function slugify(text) {
+ return text.toLowerCase()
+ .replace(/\s+/g, '-') // Replace spaces with -
+ .replace(/[^\w\-]+/g, '') // Remove all non-word chars
+ .replace(/\-\-+/g, '-') // Replace multiple - with single -
+ .replace(/^-+/, '') // Trim - from start of text
+ .replace(/-+$/, ''); // Trim - from end of text
+ }
+
+ /*****************************************************************************
+ * Markdown.Extra *
+ ****************************************************************************/
+
+ Markdown.Extra = function() {
+ // For converting internal markdown (in tables for instance).
+ // This is necessary since these methods are meant to be called as
+ // preConversion hooks, and the Markdown converter passed to init()
+ // won't convert any markdown contained in the html tags we return.
+ this.converter = null;
+
+ // Stores html blocks we generate in hooks so that
+ // they're not destroyed if the user is using a sanitizing converter
+ this.hashBlocks = [];
+
+ // Stores footnotes
+ this.footnotes = {};
+ this.usedFootnotes = [];
+
+ // Special attribute blocks for fenced code blocks and headers enabled.
+ this.attributeBlocks = false;
+
+ // Fenced code block options
+ this.googleCodePrettify = false;
+ this.highlightJs = false;
+
+ // Table options
+ this.tableClass = '';
+
+ this.tabWidth = 4;
+ };
+
+ Markdown.Extra.init = function(converter, options) {
+ // Each call to init creates a new instance of Markdown.Extra so it's
+ // safe to have multiple converters, with different options, on a single page
+ var extra = new Markdown.Extra();
+ var postNormalizationTransformations = [];
+ var preBlockGamutTransformations = [];
+ var postSpanGamutTransformations = [];
+ var postConversionTransformations = ["unHashExtraBlocks"];
+
+ options = options || {};
+ options.extensions = options.extensions || ["all"];
+ if (contains(options.extensions, "all")) {
+ options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"];
+ }
+ preBlockGamutTransformations.push("wrapHeaders");
+ if (contains(options.extensions, "attr_list")) {
+ postNormalizationTransformations.push("hashFcbAttributeBlocks");
+ preBlockGamutTransformations.push("hashHeaderAttributeBlocks");
+ postConversionTransformations.push("applyAttributeBlocks");
+ extra.attributeBlocks = true;
+ }
+ if (contains(options.extensions, "fenced_code_gfm")) {
+ // This step will convert fcb inside list items and blockquotes
+ preBlockGamutTransformations.push("fencedCodeBlocks");
+ // This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb
+ postNormalizationTransformations.push("fencedCodeBlocks");
+ }
+ if (contains(options.extensions, "tables")) {
+ preBlockGamutTransformations.push("tables");
+ }
+ if (contains(options.extensions, "def_list")) {
+ preBlockGamutTransformations.push("definitionLists");
+ }
+ if (contains(options.extensions, "footnotes")) {
+ postNormalizationTransformations.push("stripFootnoteDefinitions");
+ preBlockGamutTransformations.push("doFootnotes");
+ postConversionTransformations.push("printFootnotes");
+ }
+ if (contains(options.extensions, "smartypants")) {
+ postConversionTransformations.push("runSmartyPants");
+ }
+ if (contains(options.extensions, "strikethrough")) {
+ postSpanGamutTransformations.push("strikethrough");
+ }
+ if (contains(options.extensions, "newlines")) {
+ postSpanGamutTransformations.push("newlines");
+ }
+
+ converter.hooks.chain("postNormalization", function(text) {
+ return extra.doTransform(postNormalizationTransformations, text) + '\n';
+ });
+
+ converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) {
+ // Keep a reference to the block gamut callback to run recursively
+ extra.blockGamutHookCallback = blockGamutHookCallback;
+ text = processEscapesStep1(text);
+ text = extra.doTransform(preBlockGamutTransformations, text) + '\n';
+ text = processEscapesStep2(text);
+ return text;
+ });
+
+ converter.hooks.chain("postSpanGamut", function(text) {
+ return extra.doTransform(postSpanGamutTransformations, text);
+ });
+
+ // Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks
+ extra.previousPostConversion = converter.hooks.postConversion;
+ converter.hooks.chain("postConversion", function(text) {
+ text = extra.doTransform(postConversionTransformations, text);
+ // Clear state vars that may use unnecessary memory
+ extra.hashBlocks = [];
+ extra.footnotes = {};
+ extra.usedFootnotes = [];
+ return text;
+ });
+
+ if ("highlighter" in options) {
+ extra.googleCodePrettify = options.highlighter === 'prettify';
+ extra.highlightJs = options.highlighter === 'highlight';
+ }
+
+ if ("table_class" in options) {
+ extra.tableClass = options.table_class;
+ }
+
+ extra.converter = converter;
+
+ // Caller usually won't need this, but it's handy for testing.
+ return extra;
+ };
+
+ // Do transformations
+ Markdown.Extra.prototype.doTransform = function(transformations, text) {
+ for(var i = 0; i < transformations.length; i++)
+ text = this[transformations[i]](text);
+ return text;
+ };
+
+ // Return a placeholder containing a key, which is the block's index in the
+ // hashBlocks array. We wrap our output in a
tag here so Pagedown won't.
+ Markdown.Extra.prototype.hashExtraBlock = function(block) {
+ return '\n
~X' + (this.hashBlocks.push(block) - 1) + 'X
\n';
+ };
+ Markdown.Extra.prototype.hashExtraInline = function(block) {
+ return '~X' + (this.hashBlocks.push(block) - 1) + 'X';
+ };
+
+ // Replace placeholder blocks in `text` with their corresponding
+ // html blocks in the hashBlocks array.
+ Markdown.Extra.prototype.unHashExtraBlocks = function(text) {
+ var self = this;
+ function recursiveUnHash() {
+ var hasHash = false;
+ text = text.replace(/(?:
)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) {
+ hasHash = true;
+ var key = parseInt(m1, 10);
+ return self.hashBlocks[key];
+ });
+ if(hasHash === true) {
+ recursiveUnHash();
+ }
+ }
+ recursiveUnHash();
+ return text;
+ };
+
+ // Wrap headers to make sure they won't be in def lists
+ Markdown.Extra.prototype.wrapHeaders = function(text) {
+ function wrap(text) {
+ return '\n' + text + '\n';
+ }
+ text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap);
+ text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap);
+ text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap);
+ return text;
+ };
+
+
+ /******************************************************************
+ * Attribute Blocks *
+ *****************************************************************/
+
+ // TODO: use sentinels. Should we just add/remove them in doConversion?
+ // TODO: better matches for id / class attributes
+ var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}";
+ var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm");
+ var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
+ "(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead
+ var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
+ "(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm");
+
+ // Extract headers attribute blocks, move them above the element they will be
+ // applied to, and hash them for later.
+ Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) {
+
+ var self = this;
+ function attributeCallback(wholeMatch, pre, attr) {
+ return '
~XX' + (self.hashBlocks.push(attr) - 1) + 'XX
\n' + pre + "\n";
+ }
+
+ text = text.replace(hdrAttributesA, attributeCallback); // ## headers
+ text = text.replace(hdrAttributesB, attributeCallback); // underline headers
+ return text;
+ };
+
+ // Extract FCB attribute blocks, move them above the element they will be
+ // applied to, and hash them for later.
+ Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) {
+ // TODO: use sentinels. Should we just add/remove them in doConversion?
+ // TODO: better matches for id / class attributes
+
+ var self = this;
+ function attributeCallback(wholeMatch, pre, attr) {
+ return '
~XX' + (self.hashBlocks.push(attr) - 1) + 'XX
\n' + pre + "\n";
+ }
+
+ return text.replace(fcbAttributes, attributeCallback);
+ };
+
+ Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
+ var self = this;
+ var blockRe = new RegExp('
~XX(\\d+)XX
[\\s]*' +
+ '(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?\\2>))', "gm");
+ text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) {
+ if (!tag) // no following header or fenced code block.
+ return '';
+
+ // get attributes list from hash
+ var key = parseInt(k, 10);
+ var attributes = self.hashBlocks[key];
+
+ // get id
+ var id = attributes.match(/#[^\s#.]+/g) || [];
+ var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : '';
+
+ // get classes and merge with existing classes
+ var classes = attributes.match(/\.[^\s#.]+/g) || [];
+ for (var i = 0; i < classes.length; i++) // Remove leading dot
+ classes[i] = classes[i].substr(1, classes[i].length - 1);
+
+ var classStr = '';
+ if (cls)
+ classes = union(classes, [cls]);
+
+ if (classes.length > 0)
+ classStr = ' class="' + classes.join(' ') + '"';
+
+ return "<" + tag + idStr + classStr + rest;
+ });
+
+ return text;
+ };
+
+ /******************************************************************
+ * Tables *
+ *****************************************************************/
+
+ // Find and convert Markdown Extra tables into html.
+ Markdown.Extra.prototype.tables = function(text) {
+ var self = this;
+
+ var leadingPipe = new RegExp(
+ ['^' ,
+ '[ ]{0,3}' , // Allowed whitespace
+ '[|]' , // Initial pipe
+ '(.+)\\n' , // $1: Header Row
+
+ '[ ]{0,3}' , // Allowed whitespace
+ '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator
+
+ '(' , // $3: Table Body
+ '(?:[ ]*[|].*\\n?)*' , // Table rows
+ ')',
+ '(?:\\n|$)' // Stop at final newline
+ ].join(''),
+ 'gm'
+ );
+
+ var noLeadingPipe = new RegExp(
+ ['^' ,
+ '[ ]{0,3}' , // Allowed whitespace
+ '(\\S.*[|].*)\\n' , // $1: Header Row
+
+ '[ ]{0,3}' , // Allowed whitespace
+ '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator
+
+ '(' , // $3: Table Body
+ '(?:.*[|].*\\n?)*' , // Table rows
+ ')' ,
+ '(?:\\n|$)' // Stop at final newline
+ ].join(''),
+ 'gm'
+ );
+
+ text = text.replace(leadingPipe, doTable);
+ text = text.replace(noLeadingPipe, doTable);
+
+ // $1 = header, $2 = separator, $3 = body
+ function doTable(match, header, separator, body, offset, string) {
+ // remove any leading pipes and whitespace
+ header = header.replace(/^ *[|]/m, '');
+ separator = separator.replace(/^ *[|]/m, '');
+ body = body.replace(/^ *[|]/gm, '');
+
+ // remove trailing pipes and whitespace
+ header = header.replace(/[|] *$/m, '');
+ separator = separator.replace(/[|] *$/m, '');
+ body = body.replace(/[|] *$/gm, '');
+
+ // determine column alignments
+ var alignspecs = separator.split(/ *[|] */);
+ var align = [];
+ for (var i = 0; i < alignspecs.length; i++) {
+ var spec = alignspecs[i];
+ if (spec.match(/^ *-+: *$/m))
+ align[i] = ' align="right"';
+ else if (spec.match(/^ *:-+: *$/m))
+ align[i] = ' align="center"';
+ else if (spec.match(/^ *:-+ *$/m))
+ align[i] = ' align="left"';
+ else align[i] = '';
+ }
+
+ // TODO: parse spans in header and rows before splitting, so that pipes
+ // inside of tags are not interpreted as separators
+ var headers = header.split(/ *[|] */);
+ var colCount = headers.length;
+
+ // build html
+ var cls = self.tableClass ? ' class="' + self.tableClass + '"' : '';
+ var html = ['
\n', '\n', '
\n'].join('');
+
+ // build column headers.
+ for (i = 0; i < colCount; i++) {
+ var headerHtml = convertSpans(trim(headers[i]), self);
+ html += ["
", headerHtml, "
\n"].join('');
+ }
+ html += "
\n\n";
+
+ // build rows
+ var rows = body.split('\n');
+ for (i = 0; i < rows.length; i++) {
+ if (rows[i].match(/^\s*$/)) // can apply to final row
+ continue;
+
+ // ensure number of rowCells matches colCount
+ var rowCells = rows[i].split(/ *[|] */);
+ var lenDiff = colCount - rowCells.length;
+ for (var j = 0; j < lenDiff; j++)
+ rowCells.push('');
+
+ html += "
\n";
+ for (j = 0; j < colCount; j++) {
+ var colHtml = convertSpans(trim(rowCells[j]), self);
+ html += ["
", colHtml, "
\n"].join('');
+ }
+ html += "
\n";
+ }
+
+ html += "
\n";
+
+ // replace html with placeholder until postConversion step
+ return self.hashExtraBlock(html);
+ }
+
+ return text;
+ };
+
+
+ /******************************************************************
+ * Footnotes *
+ *****************************************************************/
+
+ // Strip footnote, store in hashes.
+ Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) {
+ var self = this;
+
+ text = text.replace(
+ /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g,
+ function(wholeMatch, m1, m2) {
+ m1 = slugify(m1);
+ m2 += "\n";
+ m2 = m2.replace(/^[ ]{0,3}/g, "");
+ self.footnotes[m1] = m2;
+ return "\n";
+ });
+
+ return text;
+ };
+
+
+ // Find and convert footnotes references.
+ Markdown.Extra.prototype.doFootnotes = function(text) {
+ var self = this;
+ if(self.isConvertingFootnote === true) {
+ return text;
+ }
+
+ var footnoteCounter = 0;
+ text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) {
+ var id = slugify(m1);
+ var footnote = self.footnotes[id];
+ if (footnote === undefined) {
+ return wholeMatch;
+ }
+ footnoteCounter++;
+ self.usedFootnotes.push(id);
+ var html = '' + footnoteCounter
+ + '';
+ return self.hashExtraInline(html);
+ });
+
+ return text;
+ };
+
+ // Print footnotes at the end of the document
+ Markdown.Extra.prototype.printFootnotes = function(text) {
+ var self = this;
+
+ if (self.usedFootnotes.length === 0) {
+ return text;
+ }
+
+ text += '\n\n
';
+ return text;
+ };
+
+
+ /******************************************************************
+ * Fenced Code Blocks (gfm) *
+ ******************************************************************/
+
+ // Find and convert gfm-inspired fenced code blocks into html.
+ Markdown.Extra.prototype.fencedCodeBlocks = function(text) {
+ function encodeCode(code) {
+ code = code.replace(/&/g, "&");
+ code = code.replace(//g, ">");
+ // These were escaped by PageDown before postNormalization
+ code = code.replace(/~D/g, "$$");
+ code = code.replace(/~T/g, "~");
+ return code;
+ }
+
+ var self = this;
+ text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) {
+ var language = trim(m1), codeblock = m2;
+
+ // adhere to specified options
+ var preclass = self.googleCodePrettify ? ' class="prettyprint"' : '';
+ var codeclass = '';
+ if (language) {
+ if (self.googleCodePrettify || self.highlightJs) {
+ // use html5 language- class names. supported by both prettify and highlight.js
+ codeclass = ' class="language-' + language + '"';
+ } else {
+ codeclass = ' class="' + language + '"';
+ }
+ }
+
+ var html = ['
',
+ encodeCode(codeblock), '
'].join('');
+
+ // replace codeblock with placeholder until postConversion step
+ return self.hashExtraBlock(html);
+ });
+
+ return text;
+ };
+
+
+ /******************************************************************
+ * SmartyPants *
+ ******************************************************************/
+
+ Markdown.Extra.prototype.educatePants = function(text) {
+ var self = this;
+ var result = '';
+ var blockOffset = 0;
+ // Here we parse HTML in a very bad manner
+ text.replace(/(?:)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) {
+ var token = text.substring(blockOffset, offset);
+ result += self.applyPants(token);
+ self.smartyPantsLastChar = result.substring(result.length - 1);
+ blockOffset = offset + wholeMatch.length;
+ if(!m1) {
+ // Skip commentary
+ result += wholeMatch;
+ return;
+ }
+ // Skip special tags
+ if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) {
+ m4 = self.educatePants(m4);
+ }
+ else {
+ self.smartyPantsLastChar = m4.substring(m4.length - 1);
+ }
+ result += m1 + m2 + m3 + m4 + m5;
+ });
+ var lastToken = text.substring(blockOffset);
+ result += self.applyPants(lastToken);
+ self.smartyPantsLastChar = result.substring(result.length - 1);
+ return result;
+ };
+
+ function revertPants(wholeMatch, m1) {
+ var blockText = m1;
+ blockText = blockText.replace(/&\#8220;/g, "\"");
+ blockText = blockText.replace(/&\#8221;/g, "\"");
+ blockText = blockText.replace(/&\#8216;/g, "'");
+ blockText = blockText.replace(/&\#8217;/g, "'");
+ blockText = blockText.replace(/&\#8212;/g, "---");
+ blockText = blockText.replace(/&\#8211;/g, "--");
+ blockText = blockText.replace(/&\#8230;/g, "...");
+ return blockText;
+ }
+
+ Markdown.Extra.prototype.applyPants = function(text) {
+ // Dashes
+ text = text.replace(/---/g, "—").replace(/--/g, "–");
+ // Ellipses
+ text = text.replace(/\.\.\./g, "…").replace(/\.\s\.\s\./g, "…");
+ // Backticks
+ text = text.replace(/``/g, "“").replace (/''/g, "”");
+
+ if(/^'$/.test(text)) {
+ // Special case: single-character ' token
+ if(/\S/.test(this.smartyPantsLastChar)) {
+ return "’";
+ }
+ return "‘";
+ }
+ if(/^"$/.test(text)) {
+ // Special case: single-character " token
+ if(/\S/.test(this.smartyPantsLastChar)) {
+ return "”";
+ }
+ return "“";
+ }
+
+ // Special case if the very first character is a quote
+ // followed by punctuation at a non-word-break. Close the quotes by brute force:
+ text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "’");
+ text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "”");
+
+ // Special case for double sets of quotes, e.g.:
+ //
He said, "'Quoted' words in a larger quote."
+ text = text.replace(/"'(?=\w)/g, "“‘");
+ text = text.replace(/'"(?=\w)/g, "‘“");
+
+ // Special case for decade abbreviations (the '80s):
+ text = text.replace(/'(?=\d{2}s)/g, "’");
+
+ // Get most opening single quotes:
+ text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1‘");
+
+ // Single closing quotes:
+ text = text.replace(/([^\s\[\{\(\-])'/g, "$1’");
+ text = text.replace(/'(?=\s|s\b)/g, "’");
+
+ // Any remaining single quotes should be opening ones:
+ text = text.replace(/'/g, "‘");
+
+ // Get most opening double quotes:
+ text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1“");
+
+ // Double closing quotes:
+ text = text.replace(/([^\s\[\{\(\-])"/g, "$1”");
+ text = text.replace(/"(?=\s)/g, "”");
+
+ // Any remaining quotes should be opening ones.
+ text = text.replace(/"/ig, "“");
+ return text;
+ };
+
+ // Find and convert markdown extra definition lists into html.
+ Markdown.Extra.prototype.runSmartyPants = function(text) {
+ this.smartyPantsLastChar = '';
+ text = this.educatePants(text);
+ // Clean everything inside html tags (some of them may have been converted due to our rough html parsing)
+ text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants);
+ return text;
+ };
+
+ /******************************************************************
+ * Definition Lists *
+ ******************************************************************/
+
+ // Find and convert markdown extra definition lists into html.
+ Markdown.Extra.prototype.definitionLists = function(text) {
+ var wholeList = new RegExp(
+ ['(\\x02\\n?|\\n\\n)' ,
+ '(?:' ,
+ '(' , // $1 = whole list
+ '(' , // $2
+ '[ ]{0,3}' ,
+ '((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term
+ '\\n?' ,
+ '[ ]{0,3}:[ ]+' , // colon starting definition
+ ')' ,
+ '([\\s\\S]+?)' ,
+ '(' , // $4
+ '(?=\\0x03)' , // \z
+ '|' ,
+ '(?=' ,
+ '\\n{2,}' ,
+ '(?=\\S)' ,
+ '(?!' , // Negative lookahead for another term
+ '[ ]{0,3}' ,
+ '(?:\\S.*\\n)+?' , // defined term
+ '\\n?' ,
+ '[ ]{0,3}:[ ]+' , // colon starting definition
+ ')' ,
+ '(?!' , // Negative lookahead for another definition
+ '[ ]{0,3}:[ ]+' , // colon starting definition
+ ')' ,
+ ')' ,
+ ')' ,
+ ')' ,
+ ')'
+ ].join(''),
+ 'gm'
+ );
+
+ var self = this;
+ text = addAnchors(text);
+
+ text = text.replace(wholeList, function(match, pre, list) {
+ var result = trim(self.processDefListItems(list));
+ result = "
\n" + result + "\n
";
+ return pre + self.hashExtraBlock(result) + "\n\n";
+ });
+
+ return removeAnchors(text);
+ };
+
+ // Process the contents of a single definition list, splitting it
+ // into individual term and definition list items.
+ Markdown.Extra.prototype.processDefListItems = function(listStr) {
+ var self = this;
+
+ var dt = new RegExp(
+ ['(\\x02\\n?|\\n\\n+)' , // leading line
+ '(' , // definition terms = $1
+ '[ ]{0,3}' , // leading whitespace
+ '(?![:][ ]|[ ])' , // negative lookahead for a definition
+ // mark (colon) or more whitespace
+ '(?:\\S.*\\n)+?' , // actual term (not whitespace)
+ ')' ,
+ '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed
+ ].join(''), // with a definition mark
+ 'gm'
+ );
+
+ var dd = new RegExp(
+ ['\\n(\\n+)?' , // leading line = $1
+ '(' , // marker space = $2
+ '[ ]{0,3}' , // whitespace before colon
+ '[:][ ]+' , // definition mark (colon)
+ ')' ,
+ '([\\s\\S]+?)' , // definition text = $3
+ '(?=\\n*' , // stop at next definition mark,
+ '(?:' , // next term or end of text
+ '\\n[ ]{0,3}[:][ ]|' ,
+ '
|\\x03' , // \z
+ ')' ,
+ ')'
+ ].join(''),
+ 'gm'
+ );
+
+ listStr = addAnchors(listStr);
+ // trim trailing blank lines:
+ listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n");
+
+ // Process definition terms.
+ listStr = listStr.replace(dt, function(match, pre, termsStr) {
+ var terms = trim(termsStr).split("\n");
+ var text = '';
+ for (var i = 0; i < terms.length; i++) {
+ var term = terms[i];
+ // process spans inside dt
+ term = convertSpans(trim(term), self);
+ text += "\n
+Writing is a lightweight distraction-free text editor.
+Write text on the left, and the result is displayed on the right.
+
+Commands
+--------
+CTRL + D: toggle display mode (editor only, preview only or both-at-the-same-time)
+CTRL + P: print or export as PDF
+CTRL + S: save source code as .MD file
+CTRL + SHIFT + O: open .MD file
+CTRL + SHIFT + 7: send to the storage
+
+CTRL + SHIFT + L: enable / disable LaTeX (i.e. math formulas)
+CTRL + SHIFT + D: toggle dark mode
+CTRL + SHIFT + R: toggle roman (LaTex-like) or sans-serif font
+CTRL + SHIFT + H: show this help dialog
+
+F11: full-screen (in most browsers)
+
+Markdown syntax
+---------------
+#Title
+##Subtitle
+This is *italic* and this is **bold**.
+This is a [link](http://www.example.com/) and this is an .
+Write code with `...` or by adding a 4-whitespace indent to the paragraph.
+> This is a quote.
+
+LaTeX syntax
+------------
+This formula $x^2+1$ will be displayed inline.
+This formula $$x^2+1$$ will be displayed in a new paragraph.
+
+Specific syntax
+---------------
+\pagebreak will trigger a pagebreak when printing / exporting to PDF.
+
+About
+-----
+Made by @JosephErnest
+https://github.com/josephernest/writing
+Uses Pagedown, Pagedown Extra, MathJax, StackOverflow's editor code and the Computer Modern font.
+
+
+
+
+
+
+
+
+
diff --git a/assets/public/editor/mathjax-editing_writing.js b/assets/public/editor/mathjax-editing_writing.js
new file mode 100644
index 0000000..6b660b6
--- /dev/null
+++ b/assets/public/editor/mathjax-editing_writing.js
@@ -0,0 +1,375 @@
+// Comes from: http://dev.stackoverflow.com/content/js/mathjax-editing.js (MIT-License)
+// Version downloaded 2016-11-21
+//
+// Two things modified:
+//
+// - StackExchange.mathjaxEditing = (function () {
+// + function mjpd() { this.mathjaxEditing = (function () {
+// - converterObject.hooks.chain("preSafe", replaceMath);
+// + converterObject.hooks.chain("postConversion", replaceMath);
+// - return { prepareWmdForMathJax: prepareWmdForMathJax };})();
+// + return { prepareWmdForMathJax: prepareWmdForMathJax } })(); }
+
+
+"use strict";
+
+function mjpd() {
+ this.mathjaxEditing = (function () {
+ var ready = false; // true after initial typeset is complete
+ var pending = null; // non-null when typesetting has been queued
+ var inline = "$"; // the inline math delimiter
+ var blocks, start, end, last, braces, indent; // used in searching for math
+ var math; // stores math until markdone is done
+ var HUB = MathJax.Hub, TEX, NOERRORS;
+
+ //
+ // Runs after initial typeset
+ //
+ HUB.Queue(function () {
+ TEX = MathJax.InputJax.TeX;
+ NOERRORS = TEX.config.noErrors;
+ ready = true;
+ HUB.processUpdateTime = 50; // reduce update time so that we can cancel easier
+ HUB.processSectionDelay = 0; // don't pause between input and output phases
+ MathJax.Extension["fast-preview"].Disable(); // disable fast-preview
+ HUB.Config({
+ // reduce chunk for more frequent updates
+ "HTML-CSS": {
+ EqnChunk: 10,
+ EqnChunkFactor: 1
+ },
+ CommonHTML: {
+ EqnChunk: 10,
+ EqnChunkFactor: 1
+ },
+ SVG: {
+ EqnChunk: 10,
+ EqnChunkFactor: 1
+ }
+ });
+ if (pending) return RestartMJ(pending, "Typeset");
+ });
+
+ //
+ // These get called before and after typsetting
+ //
+ function preTypeset() {
+ NOERRORS.disabled = true; // disable noErrors (error will be shown)
+ TEX.resetEquationNumbers(); // reset labels
+ }
+ function postTypeset() {
+ NOERRORS.disabled = false; // don't show errors when not editing
+ }
+
+ //
+ // The pattern for math delimiters and special symbols
+ // needed for searching for math in the page.
+ //
+ var SPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@|`+)/i;
+
+ //
+ // The math is in blocks i through j, so
+ // collect it into one block and clear the others.
+ // Replace &, <, and > by named entities.
+ // For IE, put at the ends of comments since IE removes \n.
+ // Clear the current math positions and store the index of the
+ // math, then push the math string onto the storage array.
+ //
+ function processMath(i, j) {
+ var block = blocks.slice(i, j + 1).join("")
+ .replace(/&/g, "&") // use HTML entity for &
+ .replace(//g, ">") // use HTML entity for >
+ ;
+ if (indent) block = block.replace(/\n /g, "\n");
+ if (HUB.Browser.isMSIE) {
+ block = block.replace(/(%[^\n]*)\n/g, "$1 \n");
+ }
+ while (j > i) blocks[j--] = "";
+ blocks[i] = "@@" + math.length + "@@";
+ math.push(block);
+ start = end = last = null;
+ }
+
+
+ var capturingStringSplit;
+ if ("aba".split(/(b)/).length === 3) {
+ capturingStringSplit = function (str, regex) { return str.split(regex); };
+ }
+ else { // IE8
+ capturingStringSplit = function (str, regex) {
+ var result = [], match;
+ if (!regex.global) {
+ var source = regex.toString(),
+ flags = "";
+ source = source.replace(/^\/(.*)\/([im]*)$/, function (wholematch, re, fl) { flags = fl; return re; });
+ regex = new RegExp(source, flags + "g");
+ }
+ regex.lastIndex = 0;
+ var lastPos = 0;
+ while ((match = regex.exec(str))) {
+ result.push(str.substring(lastPos, match.index));
+ result.push.apply(result, match.slice(1));
+ lastPos = match.index + match[0].length;
+ }
+ result.push(str.substring(lastPos));
+ return result;
+ };
+ }
+
+
+ //
+ // Break up the text into its component parts and search
+ // through them for math delimiters, braces, linebreaks, etc.
+ // Math delimiters must match and braces must balance.
+ // Don't allow math to pass through a double linebreak
+ // (which will be a paragraph).
+ // Handle backticks (don't do math inside them)
+ //
+ function removeMath(text) {
+ start = end = last = indent = null; // for tracking math delimiters
+ math = []; // stores math strings for latter
+
+ blocks = capturingStringSplit(text.replace(/\r\n?/g, "\n"), SPLIT);
+
+ for (var i = 1, m = blocks.length; i < m; i += 2) {
+ var block = blocks[i];
+ if (block.charAt(0) === "@") {
+ //
+ // Things that look like our math markers will get
+ // stored and then retrieved along with the math.
+ //
+ blocks[i] = "@@" + math.length + "@@";
+ math.push(block);
+ }
+ else if (start) {
+ //
+ // If we are in math or backticks,
+ // look for the end delimiter,
+ // but don't go past double line breaks,
+ // and balance braces within the math,
+ // but don't process math inside backticks.
+ //
+ if (block === end) {
+ if (braces > 0) {
+ last = i;
+ }
+ else if (braces === 0) {
+ processMath(start, i);
+ }
+ else {
+ start = end = last = null;
+ }
+ }
+ else if (block.match(/\n.*\n/) || i + 2 >= m) {
+ if (last) {
+ i = last;
+ if (braces >= 0) processMath(start, i);
+ }
+ start = end = last = null;
+ braces = 0;
+ }
+ else if (block === "{" && braces >= 0) {
+ braces++;
+ }
+ else if (block === "}" && braces > 0) {
+ braces--;
+ }
+ }
+ else {
+ //
+ // Look for math start delimiters and when
+ // found, set up the end delimiter.
+ //
+ if (block === inline || block === "$$") {
+ start = i;
+ end = block;
+ braces = 0;
+ }
+ else if (block.substr(1, 5) === "begin") {
+ start = i;
+ end = "\\end" + block.substr(6);
+ braces = 0;
+ }
+ else if (block.charAt(0) === "`") {
+ start = last = i;
+ end = block;
+ braces = -1; // no brace balancing
+ }
+ else if (block.charAt(0) === "\n") {
+ if (block.match(/ $/)) indent = true;
+ }
+ }
+ }
+ if (last) processMath(start, last);
+ return blocks.join("");
+ }
+
+ //
+ // Put back the math strings that were saved,
+ // and clear the math array (no need to keep it around).
+ //
+ function replaceMath(text) {
+ text = text.replace(/@@(\d+)@@/g, function (match, n) {
+ return math[n];
+ });
+ math = null;
+ return text;
+ }
+
+ //
+ // This is run to restart MathJax after it has finished
+ // the previous run (that may have been canceled)
+ //
+ function RestartMJ(preview, method) {
+ pending = false;
+ HUB.cancelTypeset = false; // won't need to do this in the future
+ HUB.Queue(
+ preTypeset,
+ [method, HUB, preview],
+ postTypeset
+ );
+ }
+
+ //
+ // When the preview changes, cancel MathJax and restart,
+ // if we haven't done that already.
+ //
+ function UpdateMJ(preview, method) {
+ if (!pending) {
+ pending = preview;
+ if (ready) {
+ HUB.Cancel();
+ HUB.Queue([RestartMJ, preview, method]);
+ }
+ }
+ }
+
+ //
+ // Save the preview ID and the inline math delimiter.
+ // Create a converter for the editor and register a preConversion hook
+ // to handle escaping the math.
+ // Create a preview refresh hook to handle starting MathJax.
+ // Check if any errors are being displayed (in case there were
+ // errors in the initial display, which doesn't go through
+ // onPreviewRefresh), and reprocess if there are.
+ //
+ function prepareWmdForMathJax(editorObject, wmdId, delimiters) {
+ var preview = document.getElementById("wmd-preview" + wmdId);
+ inline = delimiters[0][0];
+
+ var converterObject = editorObject.getConverter();
+ converterObject.hooks.chain("preConversion", removeMath);
+ converterObject.hooks.chain("postConversion", replaceMath);
+ editorObject.hooks.chain("onPreviewRefresh", function () {
+ UpdateMJ(preview, "Typeset");
+ });
+
+ HUB.Queue(function () {
+ if (preview && preview.querySelector(".mjx-noError")) {
+ RestartMJ(preview, "Reprocess");
+ }
+ });
+ }
+
+ return {
+ prepareWmdForMathJax: prepareWmdForMathJax
+ }
+ })();
+}
+//
+// Set up MathJax to allow canceling of typesetting, if it
+// doesn't already have that.
+//
+(function () {
+ var HUB = MathJax.Hub;
+
+ if (!HUB.Cancel) {
+
+ HUB.cancelTypeset = false;
+ var CANCELMESSAGE = "MathJax Canceled";
+
+ HUB.Register.StartupHook("HTML-CSS Jax Config", function () {
+ var HTMLCSS = MathJax.OutputJax["HTML-CSS"],
+ TRANSLATE = HTMLCSS.Translate;
+ HTMLCSS.Augment({
+ Translate: function (script, state) {
+ if (HUB.cancelTypeset || state.cancelled) {
+ throw Error(CANCELMESSAGE)
+ }
+ return TRANSLATE.call(HTMLCSS, script, state);
+ }
+ });
+ });
+
+ HUB.Register.StartupHook("SVG Jax Config", function () {
+ var SVG = MathJax.OutputJax["SVG"],
+ TRANSLATE = SVG.Translate;
+ SVG.Augment({
+ Translate: function (script, state) {
+ if (HUB.cancelTypeset || state.cancelled) {
+ throw Error(CANCELMESSAGE)
+ }
+ return TRANSLATE.call(SVG, script, state);
+ }
+ });
+ });
+
+ HUB.Register.StartupHook("CommonHTML Jax Config", function () {
+ var CHTML = MathJax.OutputJax.CommonHTML,
+ TRANSLATE = CHTML.Translate;
+ CHTML.Augment({
+ Translate: function (script, state) {
+ if (HUB.cancelTypeset || state.cancelled) {
+ throw Error(CANCELMESSAGE);
+ }
+ return TRANSLATE.call(CHTML, script, state);
+ }
+ });
+ });
+
+ HUB.Register.StartupHook("PreviewHTML Jax Config", function () {
+ var PHTML = MathJax.OutputJax.PreviewHTML,
+ TRANSLATE = PHTML.Translate;
+ PHTML.Augment({
+ Translate: function (script, state) {
+ if (HUB.cancelTypeset || state.cancelled) {
+ throw Error(CANCELMESSAGE);
+ }
+ return TRANSLATE.call(PHTML, script, state);
+ }
+ });
+ });
+
+ HUB.Register.StartupHook("TeX Jax Config", function () {
+ var TEX = MathJax.InputJax.TeX,
+ TRANSLATE = TEX.Translate;
+ TEX.Augment({
+ Translate: function (script, state) {
+ if (HUB.cancelTypeset || state.cancelled) {
+ throw Error(CANCELMESSAGE)
+ }
+ return TRANSLATE.call(TEX, script, state);
+ }
+ });
+ });
+
+ var PROCESSERROR = HUB.processError;
+ HUB.processError = function (error, state, type) {
+ if (error.message !== CANCELMESSAGE) {
+ return PROCESSERROR.call(HUB, error, state, type)
+ }
+ MathJax.Message.Clear(0, 0);
+ state.jaxIDs = [];
+ state.jax = {};
+ state.scripts = [];
+ state.i = state.j = 0;
+ state.cancelled = true;
+ return null;
+ };
+
+ HUB.Cancel = function () {
+ this.cancelTypeset = true;
+ };
+ }
+})();
\ No newline at end of file
diff --git a/assets/public/index.html b/assets/public/index.html
index f386a5d..fe01ea5 100644
--- a/assets/public/index.html
+++ b/assets/public/index.html
@@ -8,34 +8,16 @@