formtastic-epiceditor 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +17 -0
  3. data/Gemfile.lock +96 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +29 -0
  6. data/Rakefile +38 -0
  7. data/formtastic-epiceditor.gemspec +20 -0
  8. data/lib/formtastic-epiceditor/inputs/epic_editor_input.rb +45 -0
  9. data/lib/formtastic-epiceditor/version.rb +3 -0
  10. data/lib/formtastic-epiceditor.rb +10 -0
  11. data/lib/tasks/formtastic-epiceditor_tasks.rake +4 -0
  12. data/test/dummy/README.rdoc +261 -0
  13. data/test/dummy/Rakefile +7 -0
  14. data/test/dummy/app/assets/javascripts/application.js +15 -0
  15. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  16. data/test/dummy/app/controllers/application_controller.rb +3 -0
  17. data/test/dummy/app/helpers/application_helper.rb +2 -0
  18. data/test/dummy/app/mailers/.gitkeep +0 -0
  19. data/test/dummy/app/models/.gitkeep +0 -0
  20. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  21. data/test/dummy/config/application.rb +59 -0
  22. data/test/dummy/config/boot.rb +10 -0
  23. data/test/dummy/config/database.yml +25 -0
  24. data/test/dummy/config/environment.rb +5 -0
  25. data/test/dummy/config/environments/development.rb +37 -0
  26. data/test/dummy/config/environments/production.rb +67 -0
  27. data/test/dummy/config/environments/test.rb +37 -0
  28. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  29. data/test/dummy/config/initializers/inflections.rb +15 -0
  30. data/test/dummy/config/initializers/mime_types.rb +5 -0
  31. data/test/dummy/config/initializers/secret_token.rb +7 -0
  32. data/test/dummy/config/initializers/session_store.rb +8 -0
  33. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  34. data/test/dummy/config/locales/en.yml +5 -0
  35. data/test/dummy/config/routes.rb +58 -0
  36. data/test/dummy/config.ru +4 -0
  37. data/test/dummy/lib/assets/.gitkeep +0 -0
  38. data/test/dummy/log/.gitkeep +0 -0
  39. data/test/dummy/public/404.html +26 -0
  40. data/test/dummy/public/422.html +26 -0
  41. data/test/dummy/public/500.html +25 -0
  42. data/test/dummy/public/favicon.ico +0 -0
  43. data/test/dummy/script/rails +6 -0
  44. data/test/formtastic-epiceditor_test.rb +7 -0
  45. data/test/test_helper.rb +15 -0
  46. data/vendor/assets/images/epiceditor/edit.png +0 -0
  47. data/vendor/assets/images/epiceditor/fullscreen.png +0 -0
  48. data/vendor/assets/images/epiceditor/preview.png +0 -0
  49. data/vendor/assets/javascripts/epiceditor/epiceditor.js +2141 -0
  50. data/vendor/assets/javascripts/epiceditor/epiceditor.min.js +4 -0
  51. data/vendor/assets/stylesheets/epiceditor/_activeadmin.scss +7 -0
  52. data/vendor/assets/stylesheets/epiceditor/themes/base/epiceditor.css +50 -0
  53. data/vendor/assets/stylesheets/epiceditor/themes/editor/epic-dark.css +13 -0
  54. data/vendor/assets/stylesheets/epiceditor/themes/editor/epic-light.css +12 -0
  55. data/vendor/assets/stylesheets/epiceditor/themes/preview/bartik.css +167 -0
  56. data/vendor/assets/stylesheets/epiceditor/themes/preview/github.css +368 -0
  57. data/vendor/assets/stylesheets/epiceditor/themes/preview/preview-dark.css +121 -0
  58. metadata +148 -0
@@ -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 + '/preview.png" title="Toggle Preview Mode" class="epiceditor-toggle-btn epiceditor-toggle-preview-btn"> ' +
472
+ '<img width="30" src="' + this.settings.basePath + '/edit.png" title="Toggle Edit Mode" class="epiceditor-toggle-btn epiceditor-toggle-edit-btn"> ' +
473
+ '<img width="30" src="' + this.settings.basePath + '/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.1.1';
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
+ }());