epiceditor-rails 0.1.0

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