epic-editor-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: de76b4d665a5e13e30a9adca88df6138f110a50e
4
+ data.tar.gz: 65aa4d8d1360a3404d8047ce6284536939842023
5
+ SHA512:
6
+ metadata.gz: 762683d1eca9f4941acd19120547e6543b04c098a643738b37330cd0ec5f9c191def40249596efddf8f555ab19e8b30d85d48db6dcf5fb420b097f4c596298ef
7
+ data.tar.gz: 7ae08e4355fa96dbd3d5adff739d36e2908d50dd1b47c1f8448bcd2ad15796cddd902d394d9795f795dd0ac6b5b80aff3d897a7beaf65ebc6c8f9816e7e6f58a
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ epic-editor-rails
2
+ =================
3
+ Gemfile: ```gem 'epic-editor-rails'```
4
+ Install: $ bundle install
5
+
6
+ EpicEditor v0.2.0
7
+ http://epiceditor.com/
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../config/application', __FILE__)
2
+
3
+ EpicEditorRails::Application.load_tasks
@@ -0,0 +1,2 @@
1
+ require "epic-editor-rails/version"
2
+ require "epic-editor-rails/engine" if defined?(::Rails)
@@ -0,0 +1,8 @@
1
+ module Epic
2
+ module Editor
3
+ module Rails
4
+ class Engine < ::Rails::Engine
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Epic
2
+ module Editor
3
+ module Rails
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,2141 @@
1
+ /**
2
+ * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor)
3
+ * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed)
4
+ */
5
+
6
+ (function (window, undefined) {
7
+ /**
8
+ * Applies attributes to a DOM object
9
+ * @param {object} context The DOM obj you want to apply the attributes to
10
+ * @param {object} attrs A key/value pair of attributes you want to apply
11
+ * @returns {undefined}
12
+ */
13
+ function _applyAttrs(context, attrs) {
14
+ for (var attr in attrs) {
15
+ if (attrs.hasOwnProperty(attr)) {
16
+ context[attr] = attrs[attr];
17
+ }
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Applies styles to a DOM object
23
+ * @param {object} context The DOM obj you want to apply the attributes to
24
+ * @param {object} attrs A key/value pair of attributes you want to apply
25
+ * @returns {undefined}
26
+ */
27
+ function _applyStyles(context, attrs) {
28
+ for (var attr in attrs) {
29
+ if (attrs.hasOwnProperty(attr)) {
30
+ context.style[attr] = attrs[attr];
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Returns a DOM objects computed style
37
+ * @param {object} el The element you want to get the style from
38
+ * @param {string} styleProp The property you want to get from the element
39
+ * @returns {string} Returns a string of the value. If property is not set it will return a blank string
40
+ */
41
+ function _getStyle(el, styleProp) {
42
+ var x = el
43
+ , y = null;
44
+ if (window.getComputedStyle) {
45
+ y = document.defaultView.getComputedStyle(x, null).getPropertyValue(styleProp);
46
+ }
47
+ else if (x.currentStyle) {
48
+ y = x.currentStyle[styleProp];
49
+ }
50
+ return y;
51
+ }
52
+
53
+ /**
54
+ * Saves the current style state for the styles requested, then applys styles
55
+ * to overwrite the existing one. The old styles are returned as an object so
56
+ * you can pass it back in when you want to revert back to the old style
57
+ * @param {object} el The element to get the styles of
58
+ * @param {string} type Can be "save" or "apply". apply will just apply styles you give it. Save will write styles
59
+ * @param {object} styles Key/value style/property pairs
60
+ * @returns {object}
61
+ */
62
+ function _saveStyleState(el, type, styles) {
63
+ var returnState = {}
64
+ , style;
65
+ if (type === 'save') {
66
+ for (style in styles) {
67
+ if (styles.hasOwnProperty(style)) {
68
+ returnState[style] = _getStyle(el, style);
69
+ }
70
+ }
71
+ // After it's all done saving all the previous states, change the styles
72
+ _applyStyles(el, styles);
73
+ }
74
+ else if (type === 'apply') {
75
+ _applyStyles(el, styles);
76
+ }
77
+ return returnState;
78
+ }
79
+
80
+ /**
81
+ * Gets an elements total width including it's borders and padding
82
+ * @param {object} el The element to get the total width of
83
+ * @returns {int}
84
+ */
85
+ function _outerWidth(el) {
86
+ var b = parseInt(_getStyle(el, 'border-left-width'), 10) + parseInt(_getStyle(el, 'border-right-width'), 10)
87
+ , p = parseInt(_getStyle(el, 'padding-left'), 10) + parseInt(_getStyle(el, 'padding-right'), 10)
88
+ , w = el.offsetWidth
89
+ , t;
90
+ // For IE in case no border is set and it defaults to "medium"
91
+ if (isNaN(b)) { b = 0; }
92
+ t = b + p + w;
93
+ return t;
94
+ }
95
+
96
+ /**
97
+ * Gets an elements total height including it's borders and padding
98
+ * @param {object} el The element to get the total width of
99
+ * @returns {int}
100
+ */
101
+ function _outerHeight(el) {
102
+ var b = parseInt(_getStyle(el, 'border-top-width'), 10) + parseInt(_getStyle(el, 'border-bottom-width'), 10)
103
+ , p = parseInt(_getStyle(el, 'padding-top'), 10) + parseInt(_getStyle(el, 'padding-bottom'), 10)
104
+ , w = el.offsetHeight
105
+ , t;
106
+ // For IE in case no border is set and it defaults to "medium"
107
+ if (isNaN(b)) { b = 0; }
108
+ t = b + p + w;
109
+ return t;
110
+ }
111
+
112
+ /**
113
+ * Inserts a <link> tag specifically for CSS
114
+ * @param {string} path The path to the CSS file
115
+ * @param {object} context In what context you want to apply this to (document, iframe, etc)
116
+ * @param {string} id An id for you to reference later for changing properties of the <link>
117
+ * @returns {undefined}
118
+ */
119
+ function _insertCSSLink(path, context, id) {
120
+ id = id || '';
121
+ var headID = context.getElementsByTagName("head")[0]
122
+ , cssNode = context.createElement('link');
123
+
124
+ _applyAttrs(cssNode, {
125
+ type: 'text/css'
126
+ , id: id
127
+ , rel: 'stylesheet'
128
+ , href: path
129
+ , name: path
130
+ , media: 'screen'
131
+ });
132
+
133
+ headID.appendChild(cssNode);
134
+ }
135
+
136
+ // Simply replaces a class (o), to a new class (n) on an element provided (e)
137
+ function _replaceClass(e, o, n) {
138
+ e.className = e.className.replace(o, n);
139
+ }
140
+
141
+ // Feature detects an iframe to get the inner document for writing to
142
+ function _getIframeInnards(el) {
143
+ return el.contentDocument || el.contentWindow.document;
144
+ }
145
+
146
+ // Grabs the text from an element and preserves whitespace
147
+ function _getText(el) {
148
+ var theText;
149
+ // Make sure to check for type of string because if the body of the page
150
+ // doesn't have any text it'll be "" which is falsey and will go into
151
+ // the else which is meant for Firefox and shit will break
152
+ if (typeof document.body.innerText == 'string') {
153
+ theText = el.innerText;
154
+ }
155
+ else {
156
+ // First replace <br>s before replacing the rest of the HTML
157
+ theText = el.innerHTML.replace(/<br>/gi, "\n");
158
+ // Now we can clean the HTML
159
+ theText = theText.replace(/<(?:.|\n)*?>/gm, '');
160
+ // Now fix HTML entities
161
+ theText = theText.replace(/&lt;/gi, '<');
162
+ theText = theText.replace(/&gt;/gi, '>');
163
+ }
164
+ return theText;
165
+ }
166
+
167
+ function _setText(el, content) {
168
+ // If you want to know why we check for typeof string, see comment
169
+ // in the _getText function
170
+ if (typeof document.body.innerText == 'string') {
171
+ content = content.replace(/ /g, '\u00a0');
172
+ el.innerText = content;
173
+ }
174
+ else {
175
+ // Don't convert lt/gt characters as HTML when viewing the editor window
176
+ // TODO: Write a test to catch regressions for this
177
+ content = content.replace(/</g, '&lt;');
178
+ content = content.replace(/>/g, '&gt;');
179
+ content = content.replace(/\n/g, '<br>');
180
+ // Make sure to look for TWO spaces and replace with a space and &nbsp;
181
+ // If you find and replace every space with a &nbsp; text will not wrap.
182
+ // Hence the name (Non-Breaking-SPace).
183
+ content = content.replace(/\s\s/g, ' &nbsp;')
184
+ el.innerHTML = content;
185
+ }
186
+ return true;
187
+ }
188
+
189
+ /**
190
+ * Will return the version number if the browser is IE. If not will return -1
191
+ * TRY NEVER TO USE THIS AND USE FEATURE DETECTION IF POSSIBLE
192
+ * @returns {Number} -1 if false or the version number if true
193
+ */
194
+ function _isIE() {
195
+ var rv = -1 // Return value assumes failure.
196
+ , ua = navigator.userAgent
197
+ , re;
198
+ if (navigator.appName == 'Microsoft Internet Explorer') {
199
+ re = /MSIE ([0-9]{1,}[\.0-9]{0,})/;
200
+ if (re.exec(ua) != null) {
201
+ rv = parseFloat(RegExp.$1, 10);
202
+ }
203
+ }
204
+ return rv;
205
+ }
206
+
207
+ /**
208
+ * Same as the isIE(), but simply returns a boolean
209
+ * THIS IS TERRIBLE AND IS ONLY USED BECAUSE FULLSCREEN IN SAFARI IS BORKED
210
+ * If some other engine uses WebKit and has support for fullscreen they
211
+ * probably wont get native fullscreen until Safari's fullscreen is fixed
212
+ * @returns {Boolean} true if Safari
213
+ */
214
+ function _isSafari() {
215
+ var n = window.navigator;
216
+ return n.userAgent.indexOf('Safari') > -1 && n.userAgent.indexOf('Chrome') == -1;
217
+ }
218
+
219
+ /**
220
+ * Determines if supplied value is a function
221
+ * @param {object} object to determine type
222
+ */
223
+ function _isFunction(functionToCheck) {
224
+ var getType = {};
225
+ return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
226
+ }
227
+
228
+ /**
229
+ * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
230
+ * @param {boolean} [deepMerge=false] If true, will deep merge meaning it will merge sub-objects like {obj:obj2{foo:'bar'}}
231
+ * @param {object} first object
232
+ * @param {object} second object
233
+ * @returnss {object} a new object based on obj1 and obj2
234
+ */
235
+ function _mergeObjs() {
236
+ // copy reference to target object
237
+ var target = arguments[0] || {}
238
+ , i = 1
239
+ , length = arguments.length
240
+ , deep = false
241
+ , options
242
+ , name
243
+ , src
244
+ , copy
245
+
246
+ // Handle a deep copy situation
247
+ if (typeof target === "boolean") {
248
+ deep = target;
249
+ target = arguments[1] || {};
250
+ // skip the boolean and the target
251
+ i = 2;
252
+ }
253
+
254
+ // Handle case when target is a string or something (possible in deep copy)
255
+ if (typeof target !== "object" && !_isFunction(target)) {
256
+ target = {};
257
+ }
258
+ // extend jQuery itself if only one argument is passed
259
+ if (length === i) {
260
+ target = this;
261
+ --i;
262
+ }
263
+
264
+ for (; i < length; i++) {
265
+ // Only deal with non-null/undefined values
266
+ if ((options = arguments[i]) != null) {
267
+ // Extend the base object
268
+ for (name in options) {
269
+ // @NOTE: added hasOwnProperty check
270
+ if (options.hasOwnProperty(name)) {
271
+ src = target[name];
272
+ copy = options[name];
273
+ // Prevent never-ending loop
274
+ if (target === copy) {
275
+ continue;
276
+ }
277
+ // Recurse if we're merging object values
278
+ if (deep && copy && typeof copy === "object" && !copy.nodeType) {
279
+ target[name] = _mergeObjs(deep,
280
+ // Never move original objects, clone them
281
+ src || (copy.length != null ? [] : {})
282
+ , copy);
283
+ } else if (copy !== undefined) { // Don't bring in undefined values
284
+ target[name] = copy;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ // Return the modified object
292
+ return target;
293
+ }
294
+
295
+ /**
296
+ * Initiates the EpicEditor object and sets up offline storage as well
297
+ * @class Represents an EpicEditor instance
298
+ * @param {object} options An optional customization object
299
+ * @returns {object} EpicEditor will be returned
300
+ */
301
+ function EpicEditor(options) {
302
+ // Default settings will be overwritten/extended by options arg
303
+ var self = this
304
+ , opts = options || {}
305
+ , _defaultFileSchema
306
+ , _defaultFile
307
+ , defaults = { container: 'epiceditor'
308
+ , basePath: 'epiceditor'
309
+ , clientSideStorage: true
310
+ , localStorageName: 'epiceditor'
311
+ , useNativeFullscreen: true
312
+ , file: { name: null
313
+ , defaultContent: ''
314
+ , autoSave: 100 // Set to false for no auto saving
315
+ }
316
+ , theme: { base: '/themes/base/epiceditor.css'
317
+ , preview: '/themes/preview/github.css'
318
+ , editor: '/themes/editor/epic-dark.css'
319
+ }
320
+ , focusOnLoad: false
321
+ , shortcut: { modifier: 18 // alt keycode
322
+ , fullscreen: 70 // f keycode
323
+ , preview: 80 // p keycode
324
+ }
325
+ , parser: typeof marked == 'function' ? marked : null
326
+ }
327
+ , defaultStorage;
328
+
329
+ self.settings = _mergeObjs(true, defaults, opts);
330
+
331
+ if (!(typeof self.settings.parser == 'function' && typeof self.settings.parser('TEST') == 'string')) {
332
+ self.settings.parser = function (str) {
333
+ return str;
334
+ }
335
+ }
336
+
337
+
338
+ // Grab the container element and save it to self.element
339
+ // if it's a string assume it's an ID and if it's an object
340
+ // assume it's a DOM element
341
+ if (typeof self.settings.container == 'string') {
342
+ self.element = document.getElementById(self.settings.container);
343
+ }
344
+ else if (typeof self.settings.container == 'object') {
345
+ self.element = self.settings.container;
346
+ }
347
+
348
+ // Figure out the file name. If no file name is given we'll use the ID.
349
+ // If there's no ID either we'll use a namespaced file name that's incremented
350
+ // based on the calling order. As long as it doesn't change, drafts will be saved.
351
+ if (!self.settings.file.name) {
352
+ if (typeof self.settings.container == 'string') {
353
+ self.settings.file.name = self.settings.container;
354
+ }
355
+ else if (typeof self.settings.container == 'object') {
356
+ if (self.element.id) {
357
+ self.settings.file.name = self.element.id;
358
+ }
359
+ else {
360
+ if (!EpicEditor._data.unnamedEditors) {
361
+ EpicEditor._data.unnamedEditors = [];
362
+ }
363
+ EpicEditor._data.unnamedEditors.push(self);
364
+ self.settings.file.name = '__epiceditor-untitled-' + EpicEditor._data.unnamedEditors.length;
365
+ }
366
+ }
367
+ }
368
+
369
+ // Protect the id and overwrite if passed in as an option
370
+ // TODO: Put underscrore to denote that this is private
371
+ self._instanceId = 'epiceditor-' + Math.round(Math.random() * 100000);
372
+ self._storage = {};
373
+ self._canSave = true;
374
+
375
+ // Setup local storage of files
376
+ self._defaultFileSchema = function () {
377
+ return {
378
+ content: self.settings.file.defaultContent
379
+ , created: new Date()
380
+ , modified: new Date()
381
+ }
382
+ }
383
+
384
+ if (localStorage && self.settings.clientSideStorage) {
385
+ this._storage = localStorage;
386
+ if (this._storage[self.settings.localStorageName] && self.getFiles(self.settings.file.name) === undefined) {
387
+ _defaultFile = self.getFiles(self.settings.file.name);
388
+ _defaultFile = self._defaultFileSchema();
389
+ _defaultFile.content = self.settings.file.defaultContent;
390
+ }
391
+ }
392
+
393
+ if (!this._storage[self.settings.localStorageName]) {
394
+ defaultStorage = {};
395
+ defaultStorage[self.settings.file.name] = self._defaultFileSchema();
396
+ defaultStorage = JSON.stringify(defaultStorage);
397
+ this._storage[self.settings.localStorageName] = defaultStorage;
398
+ }
399
+
400
+ // This needs to replace the use of classes to check the state of EE
401
+ self._eeState = {
402
+ fullscreen: false
403
+ , preview: false
404
+ , edit: false
405
+ , loaded: false
406
+ , unloaded: false
407
+ }
408
+
409
+ // Now that it exists, allow binding of events if it doesn't exist yet
410
+ if (!self.events) {
411
+ self.events = {};
412
+ }
413
+
414
+ return this;
415
+ }
416
+
417
+ /**
418
+ * Inserts the EpicEditor into the DOM via an iframe and gets it ready for editing and previewing
419
+ * @returns {object} EpicEditor will be returned
420
+ */
421
+ EpicEditor.prototype.load = function (callback) {
422
+
423
+ // Get out early if it's already loaded
424
+ if (this.is('loaded')) { return this; }
425
+
426
+ // TODO: Gotta get the privates with underscores!
427
+ // TODO: Gotta document what these are for...
428
+ var self = this
429
+ , _HtmlTemplates
430
+ , iframeElement
431
+ , baseTag
432
+ , utilBtns
433
+ , utilBar
434
+ , utilBarTimer
435
+ , keypressTimer
436
+ , mousePos = { y: -1, x: -1 }
437
+ , _elementStates
438
+ , _isInEdit
439
+ , nativeFs = false
440
+ , fsElement
441
+ , isMod = false
442
+ , isCtrl = false
443
+ , eventableIframes
444
+ , i; // i is reused for loops
445
+
446
+ if (self.settings.useNativeFullscreen) {
447
+ nativeFs = document.body.webkitRequestFullScreen ? true : false
448
+ }
449
+
450
+ // Fucking Safari's native fullscreen works terribly
451
+ // REMOVE THIS IF SAFARI 7 WORKS BETTER
452
+ if (_isSafari()) {
453
+ nativeFs = false;
454
+ }
455
+
456
+ // It opens edit mode by default (for now);
457
+ if (!self.is('edit') && !self.is('preview')) {
458
+ self._eeState.edit = true;
459
+ }
460
+
461
+ callback = callback || function () {};
462
+
463
+ // The editor HTML
464
+ // TODO: edit-mode class should be dynamically added
465
+ _HtmlTemplates = {
466
+ // This is wrapping iframe element. It contains the other two iframes and the utilbar
467
+ chrome: '<div id="epiceditor-wrapper" class="epiceditor-edit-mode">' +
468
+ '<iframe frameborder="0" id="epiceditor-editor-frame"></iframe>' +
469
+ '<iframe frameborder="0" id="epiceditor-previewer-frame"></iframe>' +
470
+ '<div id="epiceditor-utilbar">' +
471
+ '<img width="30" src="' + this.settings.basePath + '/images/preview.png" title="Toggle Preview Mode" class="epiceditor-toggle-btn epiceditor-toggle-preview-btn"> ' +
472
+ '<img width="30" src="' + this.settings.basePath + '/images/edit.png" title="Toggle Edit Mode" class="epiceditor-toggle-btn epiceditor-toggle-edit-btn"> ' +
473
+ '<img width="30" src="' + this.settings.basePath + '/images/fullscreen.png" title="Enter Fullscreen" class="epiceditor-fullscreen-btn">' +
474
+ '</div>' +
475
+ '</div>'
476
+
477
+ // The previewer is just an empty box for the generated HTML to go into
478
+ , previewer: '<div id="epiceditor-preview"></div>'
479
+ };
480
+
481
+ // Write an iframe and then select it for the editor
482
+ self.element.innerHTML = '<iframe scrolling="no" frameborder="0" id= "' + self._instanceId + '"></iframe>';
483
+
484
+ // Because browsers add things like invisible padding and margins and stuff
485
+ // to iframes, we need to set manually set the height so that the height
486
+ // doesn't keep increasing (by 2px?) every time reflow() is called.
487
+ // FIXME: Figure out how to fix this without setting this
488
+ self.element.style.height = self.element.offsetHeight + 'px';
489
+
490
+ iframeElement = document.getElementById(self._instanceId);
491
+
492
+ // Store a reference to the iframeElement itself
493
+ self.iframeElement = iframeElement;
494
+
495
+ // Grab the innards of the iframe (returns the document.body)
496
+ // TODO: Change self.iframe to self.iframeDocument
497
+ self.iframe = _getIframeInnards(iframeElement);
498
+ self.iframe.open();
499
+ self.iframe.write(_HtmlTemplates.chrome);
500
+
501
+ // Now that we got the innards of the iframe, we can grab the other iframes
502
+ self.editorIframe = self.iframe.getElementById('epiceditor-editor-frame')
503
+ self.previewerIframe = self.iframe.getElementById('epiceditor-previewer-frame');
504
+
505
+ // Setup the editor iframe
506
+ self.editorIframeDocument = _getIframeInnards(self.editorIframe);
507
+ self.editorIframeDocument.open();
508
+ // Need something for... you guessed it, Firefox
509
+ self.editorIframeDocument.write('');
510
+ self.editorIframeDocument.close();
511
+
512
+ // Setup the previewer iframe
513
+ self.previewerIframeDocument = _getIframeInnards(self.previewerIframe);
514
+ self.previewerIframeDocument.open();
515
+ self.previewerIframeDocument.write(_HtmlTemplates.previewer);
516
+
517
+ // Base tag is added so that links will open a new tab and not inside of the iframes
518
+ baseTag = self.previewerIframeDocument.createElement('base');
519
+ baseTag.target = '_blank';
520
+ self.previewerIframeDocument.getElementsByTagName('head')[0].appendChild(baseTag);
521
+
522
+ self.previewerIframeDocument.close();
523
+
524
+ self.reflow();
525
+
526
+ // Insert Base Stylesheet
527
+ _insertCSSLink(self.settings.basePath + self.settings.theme.base, self.iframe, 'theme');
528
+
529
+ // Insert Editor Stylesheet
530
+ _insertCSSLink(self.settings.basePath + self.settings.theme.editor, self.editorIframeDocument, 'theme');
531
+
532
+ // Insert Previewer Stylesheet
533
+ _insertCSSLink(self.settings.basePath + self.settings.theme.preview, self.previewerIframeDocument, 'theme');
534
+
535
+ // Add a relative style to the overall wrapper to keep CSS relative to the editor
536
+ self.iframe.getElementById('epiceditor-wrapper').style.position = 'relative';
537
+
538
+ // Now grab the editor and previewer for later use
539
+ self.editor = self.editorIframeDocument.body;
540
+ self.previewer = self.previewerIframeDocument.getElementById('epiceditor-preview');
541
+
542
+ self.editor.contentEditable = true;
543
+
544
+ // Firefox's <body> gets all fucked up so, to be sure, we need to hardcode it
545
+ self.iframe.body.style.height = this.element.offsetHeight + 'px';
546
+
547
+ // Should actually check what mode it's in!
548
+ this.previewerIframe.style.display = 'none';
549
+
550
+ // FIXME figure out why it needs +2 px
551
+ if (_isIE() > -1) {
552
+ this.previewer.style.height = parseInt(_getStyle(this.previewer, 'height'), 10) + 2;
553
+ }
554
+
555
+ // If there is a file to be opened with that filename and it has content...
556
+ this.open(self.settings.file.name);
557
+
558
+ if (self.settings.focusOnLoad) {
559
+ // We need to wait until all three iframes are done loading by waiting until the parent
560
+ // iframe's ready state == complete, then we can focus on the contenteditable
561
+ self.iframe.addEventListener('readystatechange', function () {
562
+ if (self.iframe.readyState == 'complete') {
563
+ self.editorIframeDocument.body.focus();
564
+ }
565
+ });
566
+ }
567
+
568
+ utilBtns = self.iframe.getElementById('epiceditor-utilbar');
569
+
570
+ _elementStates = {}
571
+ self._goFullscreen = function (el) {
572
+
573
+ if (self.is('fullscreen')) {
574
+ self._exitFullscreen(el);
575
+ return;
576
+ }
577
+
578
+ if (nativeFs) {
579
+ el.webkitRequestFullScreen();
580
+ }
581
+
582
+ _isInEdit = self.is('edit');
583
+
584
+ // Set the state of EE in fullscreen
585
+ // We set edit and preview to true also because they're visible
586
+ // we might want to allow fullscreen edit mode without preview (like a "zen" mode)
587
+ self._eeState.fullscreen = true;
588
+ self._eeState.edit = true;
589
+ self._eeState.preview = true;
590
+
591
+ // Cache calculations
592
+ var windowInnerWidth = window.innerWidth
593
+ , windowInnerHeight = window.innerHeight
594
+ , windowOuterWidth = window.outerWidth
595
+ , windowOuterHeight = window.outerHeight;
596
+
597
+ // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66)
598
+ if (!nativeFs) {
599
+ windowOuterHeight = window.innerHeight;
600
+ }
601
+
602
+ // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper
603
+ // the editor's width wont be the same as before
604
+ _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', {
605
+ 'width': windowOuterWidth / 2 + 'px'
606
+ , 'height': windowOuterHeight + 'px'
607
+ , 'float': 'left' // Most browsers
608
+ , 'cssFloat': 'left' // FF
609
+ , 'styleFloat': 'left' // Older IEs
610
+ , 'display': 'block'
611
+ });
612
+
613
+ // the previewer
614
+ _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', {
615
+ 'width': windowOuterWidth / 2 + 'px'
616
+ , 'height': windowOuterHeight + 'px'
617
+ , 'float': 'right' // Most browsers
618
+ , 'cssFloat': 'right' // FF
619
+ , 'styleFloat': 'right' // Older IEs
620
+ , 'display': 'block'
621
+ });
622
+
623
+ // Setup the containing element CSS for fullscreen
624
+ _elementStates.element = _saveStyleState(self.element, 'save', {
625
+ 'position': 'fixed'
626
+ , 'top': '0'
627
+ , 'left': '0'
628
+ , 'width': '100%'
629
+ , 'z-index': '9999' // Most browsers
630
+ , 'zIndex': '9999' // Firefox
631
+ , 'border': 'none'
632
+ , 'margin': '0'
633
+ // Should use the base styles background!
634
+ , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below
635
+ , 'height': windowInnerHeight + 'px'
636
+ });
637
+
638
+ // The iframe element
639
+ _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', {
640
+ 'width': windowOuterWidth + 'px'
641
+ , 'height': windowInnerHeight + 'px'
642
+ });
643
+
644
+ // ...Oh, and hide the buttons and prevent scrolling
645
+ utilBtns.style.visibility = 'hidden';
646
+
647
+ if (!nativeFs) {
648
+ document.body.style.overflow = 'hidden';
649
+ }
650
+
651
+ self.preview();
652
+
653
+ self.editorIframeDocument.body.focus();
654
+
655
+ self.emit('fullscreenenter');
656
+ };
657
+
658
+ self._exitFullscreen = function (el) {
659
+ _saveStyleState(self.element, 'apply', _elementStates.element);
660
+ _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement);
661
+ _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe);
662
+ _saveStyleState(self.previewerIframe, 'apply', _elementStates.previewerIframe);
663
+
664
+ // We want to always revert back to the original styles in the CSS so,
665
+ // if it's a fluid width container it will expand on resize and not get
666
+ // stuck at a specific width after closing fullscreen.
667
+ self.element.style.width = self._eeState.reflowWidth ? self._eeState.reflowWidth : '';
668
+ self.element.style.height = self._eeState.reflowHeight ? self._eeState.reflowHeight : '';
669
+
670
+ utilBtns.style.visibility = 'visible';
671
+
672
+ if (!nativeFs) {
673
+ document.body.style.overflow = 'auto';
674
+ }
675
+ else {
676
+ document.webkitCancelFullScreen();
677
+ }
678
+ // Put the editor back in the right state
679
+ // TODO: This is ugly... how do we make this nicer?
680
+ self._eeState.fullscreen = false;
681
+
682
+ if (_isInEdit) {
683
+ self.edit();
684
+ }
685
+ else {
686
+ self.preview();
687
+ }
688
+
689
+ self.reflow();
690
+
691
+ self.emit('fullscreenexit');
692
+ };
693
+
694
+ // This setups up live previews by triggering preview() IF in fullscreen on keyup
695
+ self.editor.addEventListener('keyup', function () {
696
+ if (keypressTimer) {
697
+ window.clearTimeout(keypressTimer);
698
+ }
699
+ keypressTimer = window.setTimeout(function () {
700
+ if (self.is('fullscreen')) {
701
+ self.preview();
702
+ }
703
+ }, 250);
704
+ });
705
+
706
+ fsElement = self.iframeElement;
707
+
708
+ // Sets up the onclick event on utility buttons
709
+ utilBtns.addEventListener('click', function (e) {
710
+ var targetClass = e.target.className;
711
+ if (targetClass.indexOf('epiceditor-toggle-preview-btn') > -1) {
712
+ self.preview();
713
+ }
714
+ else if (targetClass.indexOf('epiceditor-toggle-edit-btn') > -1) {
715
+ self.edit();
716
+ }
717
+ else if (targetClass.indexOf('epiceditor-fullscreen-btn') > -1) {
718
+ self._goFullscreen(fsElement);
719
+ }
720
+ });
721
+
722
+ // Sets up the NATIVE fullscreen editor/previewer for WebKit
723
+ if (document.body.webkitRequestFullScreen) {
724
+ fsElement.addEventListener('webkitfullscreenchange', function () {
725
+ if (!document.webkitIsFullScreen) {
726
+ self._exitFullscreen(fsElement);
727
+ }
728
+ }, false);
729
+ }
730
+
731
+ utilBar = self.iframe.getElementById('epiceditor-utilbar');
732
+
733
+ // Hide it at first until they move their mouse
734
+ utilBar.style.display = 'none';
735
+
736
+ utilBar.addEventListener('mouseover', function () {
737
+ if (utilBarTimer) {
738
+ clearTimeout(utilBarTimer);
739
+ }
740
+ });
741
+
742
+ function utilBarHandler(e) {
743
+ // Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code
744
+ // we do this for 2 reasons:
745
+ // 1. On Mac OS X lion when you scroll and it does the iOS like "jump" when it hits the top/bottom of the page itll fire off
746
+ // a mousemove of a few pixels depending on how hard you scroll
747
+ // 2. We give a slight buffer to the user in case he barely touches his touchpad or mouse and not trigger the UI
748
+ if (Math.abs(mousePos.y - e.pageY) >= 5 || Math.abs(mousePos.x - e.pageX) >= 5) {
749
+ utilBar.style.display = 'block';
750
+ // if we have a timer already running, kill it out
751
+ if (utilBarTimer) {
752
+ clearTimeout(utilBarTimer);
753
+ }
754
+
755
+ // begin a new timer that hides our object after 1000 ms
756
+ utilBarTimer = window.setTimeout(function () {
757
+ utilBar.style.display = 'none';
758
+ }, 1000);
759
+ }
760
+ mousePos = { y: e.pageY, x: e.pageX };
761
+ }
762
+
763
+ // Add keyboard shortcuts for convenience.
764
+ function shortcutHandler(e) {
765
+ if (e.keyCode == self.settings.shortcut.modifier) { isMod = true } // check for modifier press(default is alt key), save to var
766
+ if (e.keyCode == 17) { isCtrl = true } // check for ctrl/cmnd press, in order to catch ctrl/cmnd + s
767
+
768
+ // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview
769
+ if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) {
770
+ e.preventDefault();
771
+ if (self.is('edit')) {
772
+ self.preview();
773
+ }
774
+ else {
775
+ self.edit();
776
+ }
777
+ }
778
+ // Check for alt+f - default shortcut to make editor fullscreen
779
+ if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen) {
780
+ e.preventDefault();
781
+ self._goFullscreen(fsElement);
782
+ }
783
+
784
+ // Set the modifier key to false once *any* key combo is completed
785
+ // or else, on Windows, hitting the alt key will lock the isMod state to true (ticket #133)
786
+ if (isMod === true && e.keyCode !== self.settings.shortcut.modifier) {
787
+ isMod = false;
788
+ }
789
+
790
+ // When a user presses "esc", revert everything!
791
+ if (e.keyCode == 27 && self.is('fullscreen')) {
792
+ self._exitFullscreen(fsElement);
793
+ }
794
+
795
+ // Check for ctrl + s (since a lot of people do it out of habit) and make it do nothing
796
+ if (isCtrl === true && e.keyCode == 83) {
797
+ self.save();
798
+ e.preventDefault();
799
+ isCtrl = false;
800
+ }
801
+
802
+ // Do the same for Mac now (metaKey == cmd).
803
+ if (e.metaKey && e.keyCode == 83) {
804
+ self.save();
805
+ e.preventDefault();
806
+ }
807
+
808
+ }
809
+
810
+ function shortcutUpHandler(e) {
811
+ if (e.keyCode == self.settings.shortcut.modifier) { isMod = false }
812
+ if (e.keyCode == 17) { isCtrl = false }
813
+ }
814
+
815
+ // Hide and show the util bar based on mouse movements
816
+ eventableIframes = [self.previewerIframeDocument, self.editorIframeDocument];
817
+
818
+ for (i = 0; i < eventableIframes.length; i++) {
819
+ eventableIframes[i].addEventListener('mousemove', function (e) {
820
+ utilBarHandler(e);
821
+ });
822
+ eventableIframes[i].addEventListener('scroll', function (e) {
823
+ utilBarHandler(e);
824
+ });
825
+ eventableIframes[i].addEventListener('keyup', function (e) {
826
+ shortcutUpHandler(e);
827
+ });
828
+ eventableIframes[i].addEventListener('keydown', function (e) {
829
+ shortcutHandler(e);
830
+ });
831
+ }
832
+
833
+ // Save the document every 100ms by default
834
+ if (self.settings.file.autoSave) {
835
+ self.saveInterval = window.setInterval(function () {
836
+ if (!self._canSave) {
837
+ return;
838
+ }
839
+ self.save();
840
+ }, self.settings.file.autoSave);
841
+ }
842
+
843
+ window.addEventListener('resize', function () {
844
+ // If NOT webkit, and in fullscreen, we need to account for browser resizing
845
+ // we don't care about webkit because you can't resize in webkit's fullscreen
846
+ if (!self.iframe.webkitRequestFullScreen && self.is('fullscreen')) {
847
+ _applyStyles(self.iframeElement, {
848
+ 'width': window.outerWidth + 'px'
849
+ , 'height': window.innerHeight + 'px'
850
+ });
851
+
852
+ _applyStyles(self.element, {
853
+ 'height': window.innerHeight + 'px'
854
+ });
855
+
856
+ _applyStyles(self.previewerIframe, {
857
+ 'width': window.outerWidth / 2 + 'px'
858
+ , 'height': window.innerHeight + 'px'
859
+ });
860
+
861
+ _applyStyles(self.editorIframe, {
862
+ 'width': window.outerWidth / 2 + 'px'
863
+ , 'height': window.innerHeight + 'px'
864
+ });
865
+ }
866
+ // Makes the editor support fluid width when not in fullscreen mode
867
+ else if (!self.is('fullscreen')) {
868
+ self.reflow();
869
+ }
870
+ });
871
+
872
+ // Set states before flipping edit and preview modes
873
+ self._eeState.loaded = true;
874
+ self._eeState.unloaded = false;
875
+
876
+ if (self.is('preview')) {
877
+ self.preview();
878
+ }
879
+ else {
880
+ self.edit();
881
+ }
882
+
883
+ self.iframe.close();
884
+ // The callback and call are the same thing, but different ways to access them
885
+ callback.call(this);
886
+ this.emit('load');
887
+ return this;
888
+ }
889
+
890
+ /**
891
+ * Will remove the editor, but not offline files
892
+ * @returns {object} EpicEditor will be returned
893
+ */
894
+ EpicEditor.prototype.unload = function (callback) {
895
+
896
+ // Make sure the editor isn't already unloaded.
897
+ if (this.is('unloaded')) {
898
+ throw new Error('Editor isn\'t loaded');
899
+ }
900
+
901
+ var self = this
902
+ , editor = window.parent.document.getElementById(self._instanceId);
903
+
904
+ editor.parentNode.removeChild(editor);
905
+ self._eeState.loaded = false;
906
+ self._eeState.unloaded = true;
907
+ callback = callback || function () {};
908
+
909
+ if (self.saveInterval) {
910
+ window.clearInterval(self.saveInterval);
911
+ }
912
+
913
+ callback.call(this);
914
+ self.emit('unload');
915
+ return self;
916
+ }
917
+
918
+ /**
919
+ * reflow allows you to dynamically re-fit the editor in the parent without
920
+ * having to unload and then reload the editor again.
921
+ *
922
+ * @param {string} kind Can either be 'width' or 'height' or null
923
+ * if null, both the height and width will be resized
924
+ *
925
+ * @returns {object} EpicEditor will be returned
926
+ */
927
+ EpicEditor.prototype.reflow = function (kind) {
928
+ var self = this
929
+ , widthDiff = _outerWidth(self.element) - self.element.offsetWidth
930
+ , heightDiff = _outerHeight(self.element) - self.element.offsetHeight
931
+ , elements = [self.iframeElement, self.editorIframe, self.previewerIframe]
932
+ , newWidth
933
+ , newHeight;
934
+
935
+
936
+ for (var x = 0; x < elements.length; x++) {
937
+ if (!kind || kind == 'width') {
938
+ newWidth = self.element.offsetWidth - widthDiff + 'px';
939
+ elements[x].style.width = newWidth;
940
+ self._eeState.reflowWidth = newWidth;
941
+ }
942
+ if (!kind || kind == 'height') {
943
+ newHeight = self.element.offsetHeight - heightDiff + 'px';
944
+ elements[x].style.height = newHeight;
945
+ self._eeState.reflowHeight = newHeight
946
+ }
947
+ }
948
+ return self;
949
+ }
950
+
951
+ /**
952
+ * Will take the markdown and generate a preview view based on the theme
953
+ * @param {string} theme The path to the theme you want to preview in
954
+ * @returns {object} EpicEditor will be returned
955
+ */
956
+ EpicEditor.prototype.preview = function (theme) {
957
+ var self = this
958
+ , x
959
+ , anchors;
960
+
961
+ theme = theme || self.settings.basePath + self.settings.theme.preview;
962
+
963
+ _replaceClass(self.getElement('wrapper'), 'epiceditor-edit-mode', 'epiceditor-preview-mode');
964
+
965
+ // Check if no CSS theme link exists
966
+ if (!self.previewerIframeDocument.getElementById('theme')) {
967
+ _insertCSSLink(theme, self.previewerIframeDocument, 'theme');
968
+ }
969
+ else if (self.previewerIframeDocument.getElementById('theme').name !== theme) {
970
+ self.previewerIframeDocument.getElementById('theme').href = theme;
971
+ }
972
+
973
+ // Add the generated HTML into the previewer
974
+ self.previewer.innerHTML = self.exportFile(null, 'html');
975
+
976
+ // Because we have a <base> tag so all links open in a new window we
977
+ // need to prevent hash links from opening in a new window
978
+ anchors = self.previewer.getElementsByTagName('a');
979
+ for (x in anchors) {
980
+ // If the link is a hash AND the links hostname is the same as the
981
+ // current window's hostname (same page) then set the target to self
982
+ if (anchors[x].hash && anchors[x].hostname == window.location.hostname) {
983
+ anchors[x].target = '_self';
984
+ }
985
+ }
986
+
987
+ // Hide the editor and display the previewer
988
+ if (!self.is('fullscreen')) {
989
+ self.editorIframe.style.display = 'none';
990
+ self.previewerIframe.style.display = 'block';
991
+ self._eeState.preview = true;
992
+ self._eeState.edit = false;
993
+ self.previewerIframe.focus();
994
+ }
995
+
996
+ self.emit('preview');
997
+ return self;
998
+ }
999
+
1000
+ /**
1001
+ * Puts the editor into fullscreen mode
1002
+ * @returns {object} EpicEditor will be returned
1003
+ */
1004
+ EpicEditor.prototype.enterFullscreen = function () {
1005
+ if (this.is('fullscreen')) { return this; }
1006
+ this._goFullscreen(this.iframeElement);
1007
+ return this;
1008
+ }
1009
+
1010
+ /**
1011
+ * Closes fullscreen mode if opened
1012
+ * @returns {object} EpicEditor will be returned
1013
+ */
1014
+ EpicEditor.prototype.exitFullscreen = function () {
1015
+ if (!this.is('fullscreen')) { return this; }
1016
+ this._exitFullscreen(this.iframeElement);
1017
+ return this;
1018
+ }
1019
+
1020
+ /**
1021
+ * Hides the preview and shows the editor again
1022
+ * @returns {object} EpicEditor will be returned
1023
+ */
1024
+ EpicEditor.prototype.edit = function () {
1025
+ var self = this;
1026
+ _replaceClass(self.getElement('wrapper'), 'epiceditor-preview-mode', 'epiceditor-edit-mode');
1027
+ self._eeState.preview = false;
1028
+ self._eeState.edit = true;
1029
+ self.editorIframe.style.display = 'block';
1030
+ self.previewerIframe.style.display = 'none';
1031
+ self.editorIframe.focus();
1032
+ self.emit('edit');
1033
+ return this;
1034
+ }
1035
+
1036
+ /**
1037
+ * Grabs a specificed HTML node. Use it as a shortcut to getting the iframe contents
1038
+ * @param {String} name The name of the node (can be document, body, editor, previewer, or wrapper)
1039
+ * @returns {Object|Null}
1040
+ */
1041
+ EpicEditor.prototype.getElement = function (name) {
1042
+ var available = {
1043
+ "container": this.element
1044
+ , "wrapper": this.iframe.getElementById('epiceditor-wrapper')
1045
+ , "wrapperIframe": this.iframeElement
1046
+ , "editor": this.editorIframeDocument
1047
+ , "editorIframe": this.editorIframe
1048
+ , "previewer": this.previewerIframeDocument
1049
+ , "previewerIframe": this.previewerIframe
1050
+ }
1051
+
1052
+ // Check that the given string is a possible option and verify the editor isn't unloaded
1053
+ // without this, you'd be given a reference to an object that no longer exists in the DOM
1054
+ if (!available[name] || this.is('unloaded')) {
1055
+ return null;
1056
+ }
1057
+ else {
1058
+ return available[name];
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * Returns a boolean of each "state" of the editor. For example "editor.is('loaded')" // returns true/false
1064
+ * @param {String} what the state you want to check for
1065
+ * @returns {Boolean}
1066
+ */
1067
+ EpicEditor.prototype.is = function (what) {
1068
+ var self = this;
1069
+ switch (what) {
1070
+ case 'loaded':
1071
+ return self._eeState.loaded;
1072
+ case 'unloaded':
1073
+ return self._eeState.unloaded
1074
+ case 'preview':
1075
+ return self._eeState.preview
1076
+ case 'edit':
1077
+ return self._eeState.edit;
1078
+ case 'fullscreen':
1079
+ return self._eeState.fullscreen;
1080
+ default:
1081
+ return false;
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * Opens a file
1087
+ * @param {string} name The name of the file you want to open
1088
+ * @returns {object} EpicEditor will be returned
1089
+ */
1090
+ EpicEditor.prototype.open = function (name) {
1091
+ var self = this
1092
+ , defaultContent = self.settings.file.defaultContent
1093
+ , fileObj;
1094
+ name = name || self.settings.file.name;
1095
+ self.settings.file.name = name;
1096
+ if (this._storage[self.settings.localStorageName]) {
1097
+ fileObj = self.getFiles();
1098
+ if (fileObj[name] !== undefined) {
1099
+ _setText(self.editor, fileObj[name].content);
1100
+ self.emit('read');
1101
+ }
1102
+ else {
1103
+ _setText(self.editor, defaultContent);
1104
+ self.save(); // ensure a save
1105
+ self.emit('create');
1106
+ }
1107
+ self.previewer.innerHTML = self.exportFile(null, 'html');
1108
+ self.emit('open');
1109
+ }
1110
+ return this;
1111
+ }
1112
+
1113
+ /**
1114
+ * Saves content for offline use
1115
+ * @returns {object} EpicEditor will be returned
1116
+ */
1117
+ EpicEditor.prototype.save = function () {
1118
+ var self = this
1119
+ , storage
1120
+ , isUpdate = false
1121
+ , file = self.settings.file.name
1122
+ , content = _getText(this.editor);
1123
+
1124
+ // This could have been false but since we're manually saving
1125
+ // we know it's save to start autoSaving again
1126
+ this._canSave = true;
1127
+
1128
+ storage = JSON.parse(this._storage[self.settings.localStorageName]);
1129
+
1130
+ // If the file doesn't exist we need to create it
1131
+ if (storage[file] === undefined) {
1132
+ storage[file] = self._defaultFileSchema();
1133
+ }
1134
+
1135
+ // If it does, we need to check if the content is different and
1136
+ // if it is, send the update event and update the timestamp
1137
+ else if (content !== storage[file].content) {
1138
+ storage[file].modified = new Date();
1139
+ isUpdate = true;
1140
+ }
1141
+
1142
+ storage[file].content = content;
1143
+ this._storage[self.settings.localStorageName] = JSON.stringify(storage);
1144
+
1145
+ // After the content is actually changed, emit update so it emits the updated content
1146
+ if (isUpdate) {
1147
+ self.emit('update');
1148
+ }
1149
+
1150
+ this.emit('save');
1151
+ return this;
1152
+ }
1153
+
1154
+ /**
1155
+ * Removes a page
1156
+ * @param {string} name The name of the file you want to remove from localStorage
1157
+ * @returns {object} EpicEditor will be returned
1158
+ */
1159
+ EpicEditor.prototype.remove = function (name) {
1160
+ var self = this
1161
+ , s;
1162
+ name = name || self.settings.file.name;
1163
+
1164
+ // If you're trying to delete a page you have open, block saving
1165
+ if (name == self.settings.file.name) {
1166
+ self._canSave = false;
1167
+ }
1168
+
1169
+ s = JSON.parse(this._storage[self.settings.localStorageName]);
1170
+ delete s[name];
1171
+ this._storage[self.settings.localStorageName] = JSON.stringify(s);
1172
+ this.emit('remove');
1173
+ return this;
1174
+ };
1175
+
1176
+ /**
1177
+ * Renames a file
1178
+ * @param {string} oldName The old file name
1179
+ * @param {string} newName The new file name
1180
+ * @returns {object} EpicEditor will be returned
1181
+ */
1182
+ EpicEditor.prototype.rename = function (oldName, newName) {
1183
+ var self = this
1184
+ , s = JSON.parse(this._storage[self.settings.localStorageName]);
1185
+ s[newName] = s[oldName];
1186
+ delete s[oldName];
1187
+ this._storage[self.settings.localStorageName] = JSON.stringify(s);
1188
+ self.open(newName);
1189
+ return this;
1190
+ };
1191
+
1192
+ /**
1193
+ * Imports a file and it's contents and opens it
1194
+ * @param {string} name The name of the file you want to import (will overwrite existing files!)
1195
+ * @param {string} content Content of the file you want to import
1196
+ * @param {string} kind The kind of file you want to import (TBI)
1197
+ * @param {object} meta Meta data you want to save with your file.
1198
+ * @returns {object} EpicEditor will be returned
1199
+ */
1200
+ EpicEditor.prototype.importFile = function (name, content, kind, meta) {
1201
+ var self = this
1202
+ , isNew = false;
1203
+
1204
+ name = name || self.settings.file.name;
1205
+ content = content || '';
1206
+ kind = kind || 'md';
1207
+ meta = meta || {};
1208
+
1209
+ if (JSON.parse(this._storage[self.settings.localStorageName])[name] === undefined) {
1210
+ isNew = true;
1211
+ }
1212
+
1213
+ // Set our current file to the new file and update the content
1214
+ self.settings.file.name = name;
1215
+ _setText(self.editor, content);
1216
+
1217
+ if (isNew) {
1218
+ self.emit('create');
1219
+ }
1220
+
1221
+ self.save();
1222
+
1223
+ if (self.is('fullscreen')) {
1224
+ self.preview();
1225
+ }
1226
+
1227
+ return this;
1228
+ };
1229
+
1230
+ /**
1231
+ * Exports a file as a string in a supported format
1232
+ * @param {string} name Name of the file you want to export (case sensitive)
1233
+ * @param {string} kind Kind of file you want the content in (currently supports html and text)
1234
+ * @returns {string|undefined} The content of the file in the content given or undefined if it doesn't exist
1235
+ */
1236
+ EpicEditor.prototype.exportFile = function (name, kind) {
1237
+ var self = this
1238
+ , file
1239
+ , content;
1240
+
1241
+ name = name || self.settings.file.name;
1242
+ kind = kind || 'text';
1243
+
1244
+ file = self.getFiles(name);
1245
+
1246
+ // If the file doesn't exist just return early with undefined
1247
+ if (file === undefined) {
1248
+ return;
1249
+ }
1250
+
1251
+ content = file.content;
1252
+
1253
+ switch (kind) {
1254
+ case 'html':
1255
+ // Get this, 2 spaces in a content editable actually converts to:
1256
+ // 0020 00a0, meaning, "space no-break space". So, manually convert
1257
+ // no-break spaces to spaces again before handing to marked.
1258
+ // Also, WebKit converts no-break to unicode equivalent and FF HTML.
1259
+ content = content.replace(/\u00a0/g, ' ').replace(/&nbsp;/g, ' ');
1260
+ return self.settings.parser(content);
1261
+ case 'text':
1262
+ content = content.replace(/\u00a0/g, ' ').replace(/&nbsp;/g, ' ');
1263
+ return content;
1264
+ default:
1265
+ return content;
1266
+ }
1267
+ }
1268
+
1269
+ EpicEditor.prototype.getFiles = function (name) {
1270
+ var files = JSON.parse(this._storage[this.settings.localStorageName]);
1271
+ if (name) {
1272
+ return files[name];
1273
+ }
1274
+ else {
1275
+ return files;
1276
+ }
1277
+ }
1278
+
1279
+ // EVENTS
1280
+ // TODO: Support for namespacing events like "preview.foo"
1281
+ /**
1282
+ * Sets up an event handler for a specified event
1283
+ * @param {string} ev The event name
1284
+ * @param {function} handler The callback to run when the event fires
1285
+ * @returns {object} EpicEditor will be returned
1286
+ */
1287
+ EpicEditor.prototype.on = function (ev, handler) {
1288
+ var self = this;
1289
+ if (!this.events[ev]) {
1290
+ this.events[ev] = [];
1291
+ }
1292
+ this.events[ev].push(handler);
1293
+ return self;
1294
+ };
1295
+
1296
+ /**
1297
+ * This will emit or "trigger" an event specified
1298
+ * @param {string} ev The event name
1299
+ * @param {any} data Any data you want to pass into the callback
1300
+ * @returns {object} EpicEditor will be returned
1301
+ */
1302
+ EpicEditor.prototype.emit = function (ev, data) {
1303
+ var self = this
1304
+ , x;
1305
+
1306
+ data = data || self.getFiles(self.settings.file.name);
1307
+
1308
+ if (!this.events[ev]) {
1309
+ return;
1310
+ }
1311
+
1312
+ function invokeHandler(handler) {
1313
+ handler.call(self, data);
1314
+ }
1315
+
1316
+ for (x = 0; x < self.events[ev].length; x++) {
1317
+ invokeHandler(self.events[ev][x]);
1318
+ }
1319
+
1320
+ return self;
1321
+ };
1322
+
1323
+ /**
1324
+ * Will remove any listeners added from EpicEditor.on()
1325
+ * @param {string} ev The event name
1326
+ * @param {function} handler Handler to remove
1327
+ * @returns {object} EpicEditor will be returned
1328
+ */
1329
+ EpicEditor.prototype.removeListener = function (ev, handler) {
1330
+ var self = this;
1331
+ if (!handler) {
1332
+ this.events[ev] = [];
1333
+ return self;
1334
+ }
1335
+ if (!this.events[ev]) {
1336
+ return self;
1337
+ }
1338
+ // Otherwise a handler and event exist, so take care of it
1339
+ this.events[ev].splice(this.events[ev].indexOf(handler), 1);
1340
+ return self;
1341
+ }
1342
+
1343
+ EpicEditor.version = '0.2.0';
1344
+
1345
+ // Used to store information to be shared across editors
1346
+ EpicEditor._data = {};
1347
+
1348
+ window.EpicEditor = EpicEditor;
1349
+ })(window);
1350
+
1351
+ /**
1352
+ * marked - A markdown parser (https://github.com/chjj/marked)
1353
+ * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed)
1354
+ */
1355
+
1356
+ ;(function() {
1357
+
1358
+ /**
1359
+ * Block-Level Grammar
1360
+ */
1361
+
1362
+ var block = {
1363
+ newline: /^\n+/,
1364
+ code: /^( {4}[^\n]+\n*)+/,
1365
+ fences: noop,
1366
+ hr: /^( *[-*_]){3,} *(?:\n+|$)/,
1367
+ heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
1368
+ lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
1369
+ blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
1370
+ list: /^( *)(bull) [^\0]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
1371
+ html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
1372
+ def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
1373
+ paragraph: /^([^\n]+\n?(?!body))+\n*/,
1374
+ text: /^[^\n]+/
1375
+ };
1376
+
1377
+ block.bullet = /(?:[*+-]|\d+\.)/;
1378
+ block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
1379
+ block.item = replace(block.item, 'gm')
1380
+ (/bull/g, block.bullet)
1381
+ ();
1382
+
1383
+ block.list = replace(block.list)
1384
+ (/bull/g, block.bullet)
1385
+ ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
1386
+ ();
1387
+
1388
+ block.html = replace(block.html)
1389
+ ('comment', /<!--[^\0]*?-->/)
1390
+ ('closed', /<(tag)[^\0]+?<\/\1>/)
1391
+ ('closing', /<tag(?!:\/|@)\b(?:"[^"]*"|'[^']*'|[^'">])*?>/)
1392
+ (/tag/g, tag())
1393
+ ();
1394
+
1395
+ block.paragraph = (function() {
1396
+ var paragraph = block.paragraph.source
1397
+ , body = [];
1398
+
1399
+ (function push(rule) {
1400
+ rule = block[rule] ? block[rule].source : rule;
1401
+ body.push(rule.replace(/(^|[^\[])\^/g, '$1'));
1402
+ return push;
1403
+ })
1404
+ ('hr')
1405
+ ('heading')
1406
+ ('lheading')
1407
+ ('blockquote')
1408
+ ('<' + tag())
1409
+ ('def');
1410
+
1411
+ return new
1412
+ RegExp(paragraph.replace('body', body.join('|')));
1413
+ })();
1414
+
1415
+ block.normal = {
1416
+ fences: block.fences,
1417
+ paragraph: block.paragraph
1418
+ };
1419
+
1420
+ block.gfm = {
1421
+ fences: /^ *``` *(\w+)? *\n([^\0]+?)\s*``` *(?:\n+|$)/,
1422
+ paragraph: /^/
1423
+ };
1424
+
1425
+ block.gfm.paragraph = replace(block.paragraph)
1426
+ ('(?!', '(?!' + block.gfm.fences.source.replace(/(^|[^\[])\^/g, '$1') + '|')
1427
+ ();
1428
+
1429
+ /**
1430
+ * Block Lexer
1431
+ */
1432
+
1433
+ block.lexer = function(src) {
1434
+ var tokens = [];
1435
+
1436
+ tokens.links = {};
1437
+
1438
+ src = src
1439
+ .replace(/\r\n|\r/g, '\n')
1440
+ .replace(/\t/g, ' ');
1441
+
1442
+ return block.token(src, tokens, true);
1443
+ };
1444
+
1445
+ block.token = function(src, tokens, top) {
1446
+ var src = src.replace(/^ +$/gm, '')
1447
+ , next
1448
+ , loose
1449
+ , cap
1450
+ , item
1451
+ , space
1452
+ , i
1453
+ , l;
1454
+
1455
+ while (src) {
1456
+ // newline
1457
+ if (cap = block.newline.exec(src)) {
1458
+ src = src.substring(cap[0].length);
1459
+ if (cap[0].length > 1) {
1460
+ tokens.push({
1461
+ type: 'space'
1462
+ });
1463
+ }
1464
+ }
1465
+
1466
+ // code
1467
+ if (cap = block.code.exec(src)) {
1468
+ src = src.substring(cap[0].length);
1469
+ cap = cap[0].replace(/^ {4}/gm, '');
1470
+ tokens.push({
1471
+ type: 'code',
1472
+ text: !options.pedantic
1473
+ ? cap.replace(/\n+$/, '')
1474
+ : cap
1475
+ });
1476
+ continue;
1477
+ }
1478
+
1479
+ // fences (gfm)
1480
+ if (cap = block.fences.exec(src)) {
1481
+ src = src.substring(cap[0].length);
1482
+ tokens.push({
1483
+ type: 'code',
1484
+ lang: cap[1],
1485
+ text: cap[2]
1486
+ });
1487
+ continue;
1488
+ }
1489
+
1490
+ // heading
1491
+ if (cap = block.heading.exec(src)) {
1492
+ src = src.substring(cap[0].length);
1493
+ tokens.push({
1494
+ type: 'heading',
1495
+ depth: cap[1].length,
1496
+ text: cap[2]
1497
+ });
1498
+ continue;
1499
+ }
1500
+
1501
+ // lheading
1502
+ if (cap = block.lheading.exec(src)) {
1503
+ src = src.substring(cap[0].length);
1504
+ tokens.push({
1505
+ type: 'heading',
1506
+ depth: cap[2] === '=' ? 1 : 2,
1507
+ text: cap[1]
1508
+ });
1509
+ continue;
1510
+ }
1511
+
1512
+ // hr
1513
+ if (cap = block.hr.exec(src)) {
1514
+ src = src.substring(cap[0].length);
1515
+ tokens.push({
1516
+ type: 'hr'
1517
+ });
1518
+ continue;
1519
+ }
1520
+
1521
+ // blockquote
1522
+ if (cap = block.blockquote.exec(src)) {
1523
+ src = src.substring(cap[0].length);
1524
+
1525
+ tokens.push({
1526
+ type: 'blockquote_start'
1527
+ });
1528
+
1529
+ cap = cap[0].replace(/^ *> ?/gm, '');
1530
+
1531
+ // Pass `top` to keep the current
1532
+ // "toplevel" state. This is exactly
1533
+ // how markdown.pl works.
1534
+ block.token(cap, tokens, top);
1535
+
1536
+ tokens.push({
1537
+ type: 'blockquote_end'
1538
+ });
1539
+
1540
+ continue;
1541
+ }
1542
+
1543
+ // list
1544
+ if (cap = block.list.exec(src)) {
1545
+ src = src.substring(cap[0].length);
1546
+
1547
+ tokens.push({
1548
+ type: 'list_start',
1549
+ ordered: isFinite(cap[2])
1550
+ });
1551
+
1552
+ // Get each top-level item.
1553
+ cap = cap[0].match(block.item);
1554
+
1555
+ next = false;
1556
+ l = cap.length;
1557
+ i = 0;
1558
+
1559
+ for (; i < l; i++) {
1560
+ item = cap[i];
1561
+
1562
+ // Remove the list item's bullet
1563
+ // so it is seen as the next token.
1564
+ space = item.length;
1565
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
1566
+
1567
+ // Outdent whatever the
1568
+ // list item contains. Hacky.
1569
+ if (~item.indexOf('\n ')) {
1570
+ space -= item.length;
1571
+ item = !options.pedantic
1572
+ ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
1573
+ : item.replace(/^ {1,4}/gm, '');
1574
+ }
1575
+
1576
+ // Determine whether item is loose or not.
1577
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
1578
+ // for discount behavior.
1579
+ loose = next || /\n\n(?!\s*$)/.test(item);
1580
+ if (i !== l - 1) {
1581
+ next = item[item.length-1] === '\n';
1582
+ if (!loose) loose = next;
1583
+ }
1584
+
1585
+ tokens.push({
1586
+ type: loose
1587
+ ? 'loose_item_start'
1588
+ : 'list_item_start'
1589
+ });
1590
+
1591
+ // Recurse.
1592
+ block.token(item, tokens);
1593
+
1594
+ tokens.push({
1595
+ type: 'list_item_end'
1596
+ });
1597
+ }
1598
+
1599
+ tokens.push({
1600
+ type: 'list_end'
1601
+ });
1602
+
1603
+ continue;
1604
+ }
1605
+
1606
+ // html
1607
+ if (cap = block.html.exec(src)) {
1608
+ src = src.substring(cap[0].length);
1609
+ tokens.push({
1610
+ type: 'html',
1611
+ pre: cap[1] === 'pre',
1612
+ text: cap[0]
1613
+ });
1614
+ continue;
1615
+ }
1616
+
1617
+ // def
1618
+ if (top && (cap = block.def.exec(src))) {
1619
+ src = src.substring(cap[0].length);
1620
+ tokens.links[cap[1].toLowerCase()] = {
1621
+ href: cap[2],
1622
+ title: cap[3]
1623
+ };
1624
+ continue;
1625
+ }
1626
+
1627
+ // top-level paragraph
1628
+ if (top && (cap = block.paragraph.exec(src))) {
1629
+ src = src.substring(cap[0].length);
1630
+ tokens.push({
1631
+ type: 'paragraph',
1632
+ text: cap[0]
1633
+ });
1634
+ continue;
1635
+ }
1636
+
1637
+ // text
1638
+ if (cap = block.text.exec(src)) {
1639
+ // Top-level should never reach here.
1640
+ src = src.substring(cap[0].length);
1641
+ tokens.push({
1642
+ type: 'text',
1643
+ text: cap[0]
1644
+ });
1645
+ continue;
1646
+ }
1647
+ }
1648
+
1649
+ return tokens;
1650
+ };
1651
+
1652
+ /**
1653
+ * Inline Processing
1654
+ */
1655
+
1656
+ var inline = {
1657
+ escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
1658
+ autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
1659
+ url: noop,
1660
+ tag: /^<!--[^\0]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
1661
+ link: /^!?\[(inside)\]\(href\)/,
1662
+ reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
1663
+ nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
1664
+ strong: /^__([^\0]+?)__(?!_)|^\*\*([^\0]+?)\*\*(?!\*)/,
1665
+ em: /^\b_((?:__|[^\0])+?)_\b|^\*((?:\*\*|[^\0])+?)\*(?!\*)/,
1666
+ code: /^(`+)([^\0]*?[^`])\1(?!`)/,
1667
+ br: /^ {2,}\n(?!\s*$)/,
1668
+ text: /^[^\0]+?(?=[\\<!\[_*`]| {2,}\n|$)/
1669
+ };
1670
+
1671
+ inline._linkInside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
1672
+ inline._linkHref = /\s*<?([^\s]*?)>?(?:\s+['"]([^\0]*?)['"])?\s*/;
1673
+
1674
+ inline.link = replace(inline.link)
1675
+ ('inside', inline._linkInside)
1676
+ ('href', inline._linkHref)
1677
+ ();
1678
+
1679
+ inline.reflink = replace(inline.reflink)
1680
+ ('inside', inline._linkInside)
1681
+ ();
1682
+
1683
+ inline.normal = {
1684
+ url: inline.url,
1685
+ strong: inline.strong,
1686
+ em: inline.em,
1687
+ text: inline.text
1688
+ };
1689
+
1690
+ inline.pedantic = {
1691
+ strong: /^__(?=\S)([^\0]*?\S)__(?!_)|^\*\*(?=\S)([^\0]*?\S)\*\*(?!\*)/,
1692
+ em: /^_(?=\S)([^\0]*?\S)_(?!_)|^\*(?=\S)([^\0]*?\S)\*(?!\*)/
1693
+ };
1694
+
1695
+ inline.gfm = {
1696
+ url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,
1697
+ text: /^[^\0]+?(?=[\\<!\[_*`]|https?:\/\/| {2,}\n|$)/
1698
+ };
1699
+
1700
+ /**
1701
+ * Inline Lexer
1702
+ */
1703
+
1704
+ inline.lexer = function(src) {
1705
+ var out = ''
1706
+ , links = tokens.links
1707
+ , link
1708
+ , text
1709
+ , href
1710
+ , cap;
1711
+
1712
+ while (src) {
1713
+ // escape
1714
+ if (cap = inline.escape.exec(src)) {
1715
+ src = src.substring(cap[0].length);
1716
+ out += cap[1];
1717
+ continue;
1718
+ }
1719
+
1720
+ // autolink
1721
+ if (cap = inline.autolink.exec(src)) {
1722
+ src = src.substring(cap[0].length);
1723
+ if (cap[2] === '@') {
1724
+ text = cap[1][6] === ':'
1725
+ ? mangle(cap[1].substring(7))
1726
+ : mangle(cap[1]);
1727
+ href = mangle('mailto:') + text;
1728
+ } else {
1729
+ text = escape(cap[1]);
1730
+ href = text;
1731
+ }
1732
+ out += '<a href="'
1733
+ + href
1734
+ + '">'
1735
+ + text
1736
+ + '</a>';
1737
+ continue;
1738
+ }
1739
+
1740
+ // url (gfm)
1741
+ if (cap = inline.url.exec(src)) {
1742
+ src = src.substring(cap[0].length);
1743
+ text = escape(cap[1]);
1744
+ href = text;
1745
+ out += '<a href="'
1746
+ + href
1747
+ + '">'
1748
+ + text
1749
+ + '</a>';
1750
+ continue;
1751
+ }
1752
+
1753
+ // tag
1754
+ if (cap = inline.tag.exec(src)) {
1755
+ src = src.substring(cap[0].length);
1756
+ out += options.sanitize
1757
+ ? escape(cap[0])
1758
+ : cap[0];
1759
+ continue;
1760
+ }
1761
+
1762
+ // link
1763
+ if (cap = inline.link.exec(src)) {
1764
+ src = src.substring(cap[0].length);
1765
+ out += outputLink(cap, {
1766
+ href: cap[2],
1767
+ title: cap[3]
1768
+ });
1769
+ continue;
1770
+ }
1771
+
1772
+ // reflink, nolink
1773
+ if ((cap = inline.reflink.exec(src))
1774
+ || (cap = inline.nolink.exec(src))) {
1775
+ src = src.substring(cap[0].length);
1776
+ link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
1777
+ link = links[link.toLowerCase()];
1778
+ if (!link || !link.href) {
1779
+ out += cap[0][0];
1780
+ src = cap[0].substring(1) + src;
1781
+ continue;
1782
+ }
1783
+ out += outputLink(cap, link);
1784
+ continue;
1785
+ }
1786
+
1787
+ // strong
1788
+ if (cap = inline.strong.exec(src)) {
1789
+ src = src.substring(cap[0].length);
1790
+ out += '<strong>'
1791
+ + inline.lexer(cap[2] || cap[1])
1792
+ + '</strong>';
1793
+ continue;
1794
+ }
1795
+
1796
+ // em
1797
+ if (cap = inline.em.exec(src)) {
1798
+ src = src.substring(cap[0].length);
1799
+ out += '<em>'
1800
+ + inline.lexer(cap[2] || cap[1])
1801
+ + '</em>';
1802
+ continue;
1803
+ }
1804
+
1805
+ // code
1806
+ if (cap = inline.code.exec(src)) {
1807
+ src = src.substring(cap[0].length);
1808
+ out += '<code>'
1809
+ + escape(cap[2], true)
1810
+ + '</code>';
1811
+ continue;
1812
+ }
1813
+
1814
+ // br
1815
+ if (cap = inline.br.exec(src)) {
1816
+ src = src.substring(cap[0].length);
1817
+ out += '<br>';
1818
+ continue;
1819
+ }
1820
+
1821
+ // text
1822
+ if (cap = inline.text.exec(src)) {
1823
+ src = src.substring(cap[0].length);
1824
+ out += escape(cap[0]);
1825
+ continue;
1826
+ }
1827
+ }
1828
+
1829
+ return out;
1830
+ };
1831
+
1832
+ function outputLink(cap, link) {
1833
+ if (cap[0][0] !== '!') {
1834
+ return '<a href="'
1835
+ + escape(link.href)
1836
+ + '"'
1837
+ + (link.title
1838
+ ? ' title="'
1839
+ + escape(link.title)
1840
+ + '"'
1841
+ : '')
1842
+ + '>'
1843
+ + inline.lexer(cap[1])
1844
+ + '</a>';
1845
+ } else {
1846
+ return '<img src="'
1847
+ + escape(link.href)
1848
+ + '" alt="'
1849
+ + escape(cap[1])
1850
+ + '"'
1851
+ + (link.title
1852
+ ? ' title="'
1853
+ + escape(link.title)
1854
+ + '"'
1855
+ : '')
1856
+ + '>';
1857
+ }
1858
+ }
1859
+
1860
+ /**
1861
+ * Parsing
1862
+ */
1863
+
1864
+ var tokens
1865
+ , token;
1866
+
1867
+ function next() {
1868
+ return token = tokens.pop();
1869
+ }
1870
+
1871
+ function tok() {
1872
+ switch (token.type) {
1873
+ case 'space': {
1874
+ return '';
1875
+ }
1876
+ case 'hr': {
1877
+ return '<hr>\n';
1878
+ }
1879
+ case 'heading': {
1880
+ return '<h'
1881
+ + token.depth
1882
+ + '>'
1883
+ + inline.lexer(token.text)
1884
+ + '</h'
1885
+ + token.depth
1886
+ + '>\n';
1887
+ }
1888
+ case 'code': {
1889
+ if (options.highlight) {
1890
+ token.code = options.highlight(token.text, token.lang);
1891
+ if (token.code != null && token.code !== token.text) {
1892
+ token.escaped = true;
1893
+ token.text = token.code;
1894
+ }
1895
+ }
1896
+
1897
+ if (!token.escaped) {
1898
+ token.text = escape(token.text, true);
1899
+ }
1900
+
1901
+ return '<pre><code'
1902
+ + (token.lang
1903
+ ? ' class="lang-'
1904
+ + token.lang
1905
+ + '"'
1906
+ : '')
1907
+ + '>'
1908
+ + token.text
1909
+ + '</code></pre>\n';
1910
+ }
1911
+ case 'blockquote_start': {
1912
+ var body = '';
1913
+
1914
+ while (next().type !== 'blockquote_end') {
1915
+ body += tok();
1916
+ }
1917
+
1918
+ return '<blockquote>\n'
1919
+ + body
1920
+ + '</blockquote>\n';
1921
+ }
1922
+ case 'list_start': {
1923
+ var type = token.ordered ? 'ol' : 'ul'
1924
+ , body = '';
1925
+
1926
+ while (next().type !== 'list_end') {
1927
+ body += tok();
1928
+ }
1929
+
1930
+ return '<'
1931
+ + type
1932
+ + '>\n'
1933
+ + body
1934
+ + '</'
1935
+ + type
1936
+ + '>\n';
1937
+ }
1938
+ case 'list_item_start': {
1939
+ var body = '';
1940
+
1941
+ while (next().type !== 'list_item_end') {
1942
+ body += token.type === 'text'
1943
+ ? parseText()
1944
+ : tok();
1945
+ }
1946
+
1947
+ return '<li>'
1948
+ + body
1949
+ + '</li>\n';
1950
+ }
1951
+ case 'loose_item_start': {
1952
+ var body = '';
1953
+
1954
+ while (next().type !== 'list_item_end') {
1955
+ body += tok();
1956
+ }
1957
+
1958
+ return '<li>'
1959
+ + body
1960
+ + '</li>\n';
1961
+ }
1962
+ case 'html': {
1963
+ if (options.sanitize) {
1964
+ return inline.lexer(token.text);
1965
+ }
1966
+ return !token.pre && !options.pedantic
1967
+ ? inline.lexer(token.text)
1968
+ : token.text;
1969
+ }
1970
+ case 'paragraph': {
1971
+ return '<p>'
1972
+ + inline.lexer(token.text)
1973
+ + '</p>\n';
1974
+ }
1975
+ case 'text': {
1976
+ return '<p>'
1977
+ + parseText()
1978
+ + '</p>\n';
1979
+ }
1980
+ }
1981
+ }
1982
+
1983
+ function parseText() {
1984
+ var body = token.text
1985
+ , top;
1986
+
1987
+ while ((top = tokens[tokens.length-1])
1988
+ && top.type === 'text') {
1989
+ body += '\n' + next().text;
1990
+ }
1991
+
1992
+ return inline.lexer(body);
1993
+ }
1994
+
1995
+ function parse(src) {
1996
+ tokens = src.reverse();
1997
+
1998
+ var out = '';
1999
+ while (next()) {
2000
+ out += tok();
2001
+ }
2002
+
2003
+ tokens = null;
2004
+ token = null;
2005
+
2006
+ return out;
2007
+ }
2008
+
2009
+ /**
2010
+ * Helpers
2011
+ */
2012
+
2013
+ function escape(html, encode) {
2014
+ return html
2015
+ .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
2016
+ .replace(/</g, '&lt;')
2017
+ .replace(/>/g, '&gt;')
2018
+ .replace(/"/g, '&quot;')
2019
+ .replace(/'/g, '&#39;');
2020
+ }
2021
+
2022
+ function mangle(text) {
2023
+ var out = ''
2024
+ , l = text.length
2025
+ , i = 0
2026
+ , ch;
2027
+
2028
+ for (; i < l; i++) {
2029
+ ch = text.charCodeAt(i);
2030
+ if (Math.random() > 0.5) {
2031
+ ch = 'x' + ch.toString(16);
2032
+ }
2033
+ out += '&#' + ch + ';';
2034
+ }
2035
+
2036
+ return out;
2037
+ }
2038
+
2039
+ function tag() {
2040
+ var tag = '(?!(?:'
2041
+ + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
2042
+ + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
2043
+ + '|span|br|wbr|ins|del|img)\\b)\\w+';
2044
+
2045
+ return tag;
2046
+ }
2047
+
2048
+ function replace(regex, opt) {
2049
+ regex = regex.source;
2050
+ opt = opt || '';
2051
+ return function self(name, val) {
2052
+ if (!name) return new RegExp(regex, opt);
2053
+ regex = regex.replace(name, val.source || val);
2054
+ return self;
2055
+ };
2056
+ }
2057
+
2058
+ function noop() {}
2059
+ noop.exec = noop;
2060
+
2061
+ /**
2062
+ * Marked
2063
+ */
2064
+
2065
+ function marked(src, opt) {
2066
+ setOptions(opt);
2067
+ return parse(block.lexer(src));
2068
+ }
2069
+
2070
+ /**
2071
+ * Options
2072
+ */
2073
+
2074
+ var options
2075
+ , defaults;
2076
+
2077
+ function setOptions(opt) {
2078
+ if (!opt) opt = defaults;
2079
+ if (options === opt) return;
2080
+ options = opt;
2081
+
2082
+ if (options.gfm) {
2083
+ block.fences = block.gfm.fences;
2084
+ block.paragraph = block.gfm.paragraph;
2085
+ inline.text = inline.gfm.text;
2086
+ inline.url = inline.gfm.url;
2087
+ } else {
2088
+ block.fences = block.normal.fences;
2089
+ block.paragraph = block.normal.paragraph;
2090
+ inline.text = inline.normal.text;
2091
+ inline.url = inline.normal.url;
2092
+ }
2093
+
2094
+ if (options.pedantic) {
2095
+ inline.em = inline.pedantic.em;
2096
+ inline.strong = inline.pedantic.strong;
2097
+ } else {
2098
+ inline.em = inline.normal.em;
2099
+ inline.strong = inline.normal.strong;
2100
+ }
2101
+ }
2102
+
2103
+ marked.options =
2104
+ marked.setOptions = function(opt) {
2105
+ defaults = opt;
2106
+ setOptions(opt);
2107
+ return marked;
2108
+ };
2109
+
2110
+ marked.setOptions({
2111
+ gfm: true,
2112
+ pedantic: false,
2113
+ sanitize: false,
2114
+ highlight: null
2115
+ });
2116
+
2117
+ /**
2118
+ * Expose
2119
+ */
2120
+
2121
+ marked.parser = function(src, opt) {
2122
+ setOptions(opt);
2123
+ return parse(src);
2124
+ };
2125
+
2126
+ marked.lexer = function(src, opt) {
2127
+ setOptions(opt);
2128
+ return block.lexer(src);
2129
+ };
2130
+
2131
+ marked.parse = marked;
2132
+
2133
+ if (typeof module !== 'undefined') {
2134
+ module.exports = marked;
2135
+ } else {
2136
+ this.marked = marked;
2137
+ }
2138
+
2139
+ }).call(function() {
2140
+ return this || (typeof window !== 'undefined' ? window : global);
2141
+ }());