epiceditor 0.0.0 → 0.2.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 403b36c406bbe271bd9df4be4392849a260dc549
4
- data.tar.gz: 58c14d7631aa013b99083c8acbc756b2111f8b51
3
+ metadata.gz: 7bee30a96dce43daaf633ed54e43a346f13d390a
4
+ data.tar.gz: cee080d2545c86bda902dcefeaa51dba6c050be2
5
5
  SHA512:
6
- metadata.gz: e2a092f83f2ee09e37a3b2b037facd4bdbbd1e0cfb349b921be0f5025df0b3248801f32815eefa59bf48fe64f76ef8acbf184c9fa0df2a7b0ef978a1d9cfc7d2
7
- data.tar.gz: a320fa979e9045fe9c6d61a612b5fcd5e22602e0837b5a3a728241f34fec14bcf2c982e6d713f81feb1bd050ff64cd819655675ab5d4227615cdcb6dd0f88d97
6
+ metadata.gz: 5b868ad0ecc3ab72ab45686196128d3de8c9b60b041c6bc2d501b3fef291ef6e33841dc7313491df2106af5442918bef88aa37b7b053a168dfa3b546e1d3a27a
7
+ data.tar.gz: f928a09bb0616c4ba9a8bf360dae32e32b337cc781d9663f910a7f02dac7764a304300f10ca1dcad87ccd56cafdeb3811ba4ca9b38791ce5dd7387653e2db89d
@@ -0,0 +1,4 @@
1
+ ## v0.2.2.1 (August 20, 2013)
2
+ - Version 0.2.2.1 is the initial release of EpicEditor *for Rails*. This version simply adds EpicEditor to the asset pipeline.
3
+
4
+ For more information on versioning, reference the [README](https://github.com/AJAlabs/EpicEditor#versioning)
data/LICENSE.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # MIT License
2
2
 
3
- EpicEditor for Rails
4
- Copyright (c) 2013 [AJ Acevedo](http://AJAlabs.com)
3
+ EpicEditor for Rails - Copyright (c) 2013, [AJ Acevedo](http://AJAlabs.com)
5
4
 
6
- EpicEditor
7
- Copyright (c) 2011-2013, Oscar Godson (http://oscargodson.com)
5
+ EpicEditor - Copyright (c) 2011-2013, [Oscar Godson](http://oscargodson.com)
8
6
 
9
7
  Permission is hereby granted, free of charge, to any person obtaining a copy
10
8
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,78 @@
1
1
  # EpicEditor for Rails
2
+ [![Gem Version](https://badge.fury.io/rb/epiceditor.png)](http://badge.fury.io/rb/epiceditor)
3
+ [![Build Status](https://travis-ci.org/AJAlabs/EpicEditor.png?branch=master)](https://travis-ci.org/AJAlabs/EpicEditor)
4
+ [![Code Climate](https://codeclimate.com/repos/520c836cf3ea004527039551/badges/a479df1697395a3c3040/gpa.png)](https://codeclimate.com/repos/520c836cf3ea004527039551/feed)
2
5
 
3
- EpicEditor for Rails - A JavaScript Markdown Editor for Ruby on Rails 4.0.0.
4
6
 
5
- Non-functional Work-in-Progress
7
+ EpicEditor *for Rails* - is a Ruby on Rails version of [EpicEditor](http://epiceditor.com) *"An Embeddable JavaScript Markdown Editor"*, ready to drop right into your Rails 4 applications.
6
8
 
7
- [AJ Acevedo](https://twitter.com/AJ_Acevedo)
9
+
10
+ **NOTE:** EpicEditor *for Rails* version 0.2.2.1 simply adds EpicEditor to the asset pipeline. You can [watch](https://help.github.com/articles/watching-repositories) this repo to be notified when additional features are added. Additional customization is coming soon.
11
+
12
+
13
+ ## Installation
14
+
15
+ Add the following line to your Gemfile:
16
+
17
+ ```ruby
18
+ gem 'epiceditor', '~> 0.2.2'
19
+ ```
20
+
21
+ `bundle install` and restart your server to make the files available.
22
+
23
+ Add the javascript file to `app/assets/javascripts/application.js` just above `require_tree .`
24
+
25
+ ```javascript
26
+ //= require epiceditor/epiceditor
27
+ //= require_tree .
28
+ ```
29
+
30
+ In your `application.css`, include the css file:
31
+
32
+ ```css
33
+ /*
34
+ *= require epiceditor
35
+ */
36
+ ```
37
+
38
+ If you prefer [SCSS](http://sass-lang.com/docs.html), add this to your application.css.scss file:
39
+
40
+ ```scss
41
+ @import "epiceditor";
42
+ ```
43
+
44
+ Mount the engine to your application by adding the following line to your config/routes.rb.
45
+
46
+ ```ruby
47
+ mount EpicEditor::Engine => "/"
48
+ ```
49
+
50
+ Alternatively, if you only want EpicEditor to be available at a specific path change `"/"` to `"/my_custom_path"`
51
+
52
+
53
+ ## Bugs and Feature Requests
54
+
55
+ Have a bug or a feature request? [Please open a new issue](https://github.com/AJAlabs/EpicEditor/issues). Before opening an issue, please search for existing issues in this repo as well as the [OscarGodson/EpicEditor](https://github.com/OscarGodson/EpicEditor/issues) repository.
56
+
57
+
58
+ ## Versioning
59
+ EpicEditor uses Semantic Versioning (ie: 0.2.2). EpicEditor *for Rails* version numbering will stay in sync with the EpicEditor version number, and be released in the form of `1.x.y.z`, where `1.x.y` is the release of EpicEditor, and `z` is the Rails patch version.
60
+
61
+
62
+ ## Troubleshooting
63
+
64
+ - Verify the `javascripts` and `stylesheets` assets appear within your application's asset pipeline search path by running ` y Rails.application.config.assets.paths` using `rails console`. You should see something similar to the following:
65
+
66
+ ```
67
+ .../epiceditor-0.2.2.1/vendor/assets/stylesheets
68
+ .../epiceditor-0.2.2.1/vendor/assets/javascripts
69
+ ```
70
+
71
+ ## Support
72
+
73
+ If you're having any problems with EpicEditor *for Rails*, feel free to open a [new ticket](https://github.com/AJAlabs/EpicEditor/issues/new). Go ahead and ask us anything and we'll try to help however we can. You can also see if there's someone available at the #epiceditor IRC channel on irc.freenode.net.
74
+
75
+ ## Who's Who?
76
+
77
+ EpicEditor *for Rails* is a project by [AJ Acevedo](https://twitter.com/AJ_Acevedo)
78
+ EpicEditor is a project by [Oscar Godson](https://twitter.com/oscargodson)
data/Rakefile CHANGED
@@ -1,23 +1,17 @@
1
+ #!/usr/bin/env rake
1
2
  begin
2
3
  require 'bundler/setup'
3
4
  rescue LoadError
4
5
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
6
  end
6
7
 
7
- require 'rdoc/task'
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'Epiceditor'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.rdoc')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
15
- end
16
8
 
17
9
  APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
- load 'rails/tasks/engine.rake'
19
-
20
10
 
11
+ if File.exists?(APP_RAKEFILE)
12
+ load 'rails/tasks/engine.rake'
13
+ end
21
14
 
22
15
  Bundler::GemHelper.install_tasks
23
16
 
17
+ task :default => :spec
@@ -1,4 +1,4 @@
1
- module Epiceditor
1
+ module EpicEditor
2
2
  class ApplicationController < ActionController::Base
3
3
  end
4
4
  end
@@ -1,4 +1,4 @@
1
- module Epiceditor
1
+ module EpicEditor
2
2
  module ApplicationHelper
3
3
  end
4
4
  end
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Epiceditor</title>
4
+ <title>EpicEditor for Rails</title>
5
5
  <%= stylesheet_link_tag "epiceditor/application", media: "all" %>
6
6
  <%= javascript_include_tag "epiceditor/application" %>
7
7
  <%= csrf_meta_tags %>
@@ -1,2 +1,2 @@
1
- Epiceditor::Engine.routes.draw do
1
+ EpicEditor::Engine.routes.draw do
2
2
  end
@@ -1,5 +1,5 @@
1
1
  require 'epiceditor/engine'
2
2
  require 'epiceditor/version'
3
3
 
4
- module Epiceditor
4
+ module EpicEditor
5
5
  end
@@ -1,7 +1,14 @@
1
- module Epiceditor
1
+ module EpicEditor
2
2
  class Engine < ::Rails::Engine
3
-
4
- isolate_namespace Epiceditor
3
+
4
+ isolate_namespace EpicEditor
5
+
6
+ # engine_name allows you to run rake task as the following: epiceditor:install:migrations
5
7
  engine_name 'epiceditor'
8
+
9
+ config.generators do |g|
10
+ g.test_framework :rspec
11
+ g.integration_tool :rspec
12
+ end
6
13
  end
7
14
  end
@@ -1,12 +1,13 @@
1
- module Epiceditor
1
+ module EpicEditor
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 0
5
- TINY = 0
4
+ MINOR = 2
5
+ TINY = 2
6
+ PATCH = 1
6
7
  BUILD = nil
7
- DATE = '2013-08-15'
8
+ DATE = '2013-08-20'
8
9
 
9
- # Epiceditor::Version::STRING
10
- STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
10
+ # EpicEditor::Version::STRING
11
+ STRING = [MAJOR, MINOR, TINY, PATCH, BUILD].compact.join('.')
11
12
  end
12
13
  end
@@ -0,0 +1,2899 @@
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 applies 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 = parseInt(_getStyle(el, 'height'), 10)
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
+ // Don't convert lt/gt characters as HTML when viewing the editor window
169
+ // TODO: Write a test to catch regressions for this
170
+ content = content.replace(/</g, '&lt;');
171
+ content = content.replace(/>/g, '&gt;');
172
+ content = content.replace(/\n/g, '<br>');
173
+
174
+ // Make sure to there aren't two spaces in a row (replace one with &nbsp;)
175
+ // If you find and replace every space with a &nbsp; text will not wrap.
176
+ // Hence the name (Non-Breaking-SPace).
177
+ // TODO: Probably need to test this somehow...
178
+ content = content.replace(/<br>\s/g, '<br>&nbsp;')
179
+ content = content.replace(/\s\s\s/g, '&nbsp; &nbsp;')
180
+ content = content.replace(/\s\s/g, '&nbsp; ')
181
+ content = content.replace(/^ /, '&nbsp;')
182
+
183
+ el.innerHTML = content;
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Converts the 'raw' format of a file's contents into plaintext
189
+ * @param {string} content Contents of the file
190
+ * @returns {string} the sanitized content
191
+ */
192
+ function _sanitizeRawContent(content) {
193
+ // Get this, 2 spaces in a content editable actually converts to:
194
+ // 0020 00a0, meaning, "space no-break space". So, manually convert
195
+ // no-break spaces to spaces again before handing to marked.
196
+ // Also, WebKit converts no-break to unicode equivalent and FF HTML.
197
+ return content.replace(/\u00a0/g, ' ').replace(/&nbsp;/g, ' ');
198
+ }
199
+
200
+ /**
201
+ * Will return the version number if the browser is IE. If not will return -1
202
+ * TRY NEVER TO USE THIS AND USE FEATURE DETECTION IF POSSIBLE
203
+ * @returns {Number} -1 if false or the version number if true
204
+ */
205
+ function _isIE() {
206
+ var rv = -1 // Return value assumes failure.
207
+ , ua = navigator.userAgent
208
+ , re;
209
+ if (navigator.appName == 'Microsoft Internet Explorer') {
210
+ re = /MSIE ([0-9]{1,}[\.0-9]{0,})/;
211
+ if (re.exec(ua) != null) {
212
+ rv = parseFloat(RegExp.$1, 10);
213
+ }
214
+ }
215
+ return rv;
216
+ }
217
+
218
+ /**
219
+ * Same as the isIE(), but simply returns a boolean
220
+ * THIS IS TERRIBLE AND IS ONLY USED BECAUSE FULLSCREEN IN SAFARI IS BORKED
221
+ * If some other engine uses WebKit and has support for fullscreen they
222
+ * probably wont get native fullscreen until Safari's fullscreen is fixed
223
+ * @returns {Boolean} true if Safari
224
+ */
225
+ function _isSafari() {
226
+ var n = window.navigator;
227
+ return n.userAgent.indexOf('Safari') > -1 && n.userAgent.indexOf('Chrome') == -1;
228
+ }
229
+
230
+ /**
231
+ * Same as the isIE(), but simply returns a boolean
232
+ * THIS IS TERRIBLE ONLY USE IF ABSOLUTELY NEEDED
233
+ * @returns {Boolean} true if Safari
234
+ */
235
+ function _isFirefox() {
236
+ var n = window.navigator;
237
+ return n.userAgent.indexOf('Firefox') > -1 && n.userAgent.indexOf('Seamonkey') == -1;
238
+ }
239
+
240
+ /**
241
+ * Determines if supplied value is a function
242
+ * @param {object} object to determine type
243
+ */
244
+ function _isFunction(functionToCheck) {
245
+ var getType = {};
246
+ return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
247
+ }
248
+
249
+ /**
250
+ * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
251
+ * @param {boolean} [deepMerge=false] If true, will deep merge meaning it will merge sub-objects like {obj:obj2{foo:'bar'}}
252
+ * @param {object} first object
253
+ * @param {object} second object
254
+ * @returnss {object} a new object based on obj1 and obj2
255
+ */
256
+ function _mergeObjs() {
257
+ // copy reference to target object
258
+ var target = arguments[0] || {}
259
+ , i = 1
260
+ , length = arguments.length
261
+ , deep = false
262
+ , options
263
+ , name
264
+ , src
265
+ , copy
266
+
267
+ // Handle a deep copy situation
268
+ if (typeof target === "boolean") {
269
+ deep = target;
270
+ target = arguments[1] || {};
271
+ // skip the boolean and the target
272
+ i = 2;
273
+ }
274
+
275
+ // Handle case when target is a string or something (possible in deep copy)
276
+ if (typeof target !== "object" && !_isFunction(target)) {
277
+ target = {};
278
+ }
279
+ // extend jQuery itself if only one argument is passed
280
+ if (length === i) {
281
+ target = this;
282
+ --i;
283
+ }
284
+
285
+ for (; i < length; i++) {
286
+ // Only deal with non-null/undefined values
287
+ if ((options = arguments[i]) != null) {
288
+ // Extend the base object
289
+ for (name in options) {
290
+ // @NOTE: added hasOwnProperty check
291
+ if (options.hasOwnProperty(name)) {
292
+ src = target[name];
293
+ copy = options[name];
294
+ // Prevent never-ending loop
295
+ if (target === copy) {
296
+ continue;
297
+ }
298
+ // Recurse if we're merging object values
299
+ if (deep && copy && typeof copy === "object" && !copy.nodeType) {
300
+ target[name] = _mergeObjs(deep,
301
+ // Never move original objects, clone them
302
+ src || (copy.length != null ? [] : {})
303
+ , copy);
304
+ } else if (copy !== undefined) { // Don't bring in undefined values
305
+ target[name] = copy;
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ // Return the modified object
313
+ return target;
314
+ }
315
+
316
+ /**
317
+ * Initiates the EpicEditor object and sets up offline storage as well
318
+ * @class Represents an EpicEditor instance
319
+ * @param {object} options An optional customization object
320
+ * @returns {object} EpicEditor will be returned
321
+ */
322
+ function EpicEditor(options) {
323
+ // Default settings will be overwritten/extended by options arg
324
+ var self = this
325
+ , opts = options || {}
326
+ , _defaultFileSchema
327
+ , _defaultFile
328
+ , defaults = { container: 'epiceditor'
329
+ , basePath: 'epiceditor'
330
+ , textarea: undefined
331
+ , clientSideStorage: true
332
+ , localStorageName: 'epiceditor'
333
+ , useNativeFullscreen: true
334
+ , file: { name: null
335
+ , defaultContent: ''
336
+ , autoSave: 100 // Set to false for no auto saving
337
+ }
338
+ , theme: { base: '/themes/base/epiceditor.css'
339
+ , preview: '/themes/preview/github.css'
340
+ , editor: '/themes/editor/epic-dark.css'
341
+ }
342
+ , focusOnLoad: false
343
+ , shortcut: { modifier: 18 // alt keycode
344
+ , fullscreen: 70 // f keycode
345
+ , preview: 80 // p keycode
346
+ }
347
+ , string: { togglePreview: 'Toggle Preview Mode'
348
+ , toggleEdit: 'Toggle Edit Mode'
349
+ , toggleFullscreen: 'Enter Fullscreen'
350
+ }
351
+ , parser: typeof marked == 'function' ? marked : null
352
+ , autogrow: false
353
+ , button: { fullscreen: true
354
+ , preview: true
355
+ , bar: "auto"
356
+ }
357
+ }
358
+ , defaultStorage
359
+ , autogrowDefaults = { minHeight: 80
360
+ , maxHeight: false
361
+ , scroll: true
362
+ };
363
+
364
+ self.settings = _mergeObjs(true, defaults, opts);
365
+
366
+ var buttons = self.settings.button;
367
+ self._fullscreenEnabled = typeof(buttons) === 'object' ? typeof buttons.fullscreen === 'undefined' || buttons.fullscreen : buttons === true;
368
+ self._editEnabled = typeof(buttons) === 'object' ? typeof buttons.edit === 'undefined' || buttons.edit : buttons === true;
369
+ self._previewEnabled = typeof(buttons) === 'object' ? typeof buttons.preview === 'undefined' || buttons.preview : buttons === true;
370
+
371
+ if (!(typeof self.settings.parser == 'function' && typeof self.settings.parser('TEST') == 'string')) {
372
+ self.settings.parser = function (str) {
373
+ return str;
374
+ }
375
+ }
376
+
377
+ if (self.settings.autogrow) {
378
+ if (self.settings.autogrow === true) {
379
+ self.settings.autogrow = autogrowDefaults;
380
+ }
381
+ else {
382
+ self.settings.autogrow = _mergeObjs(true, autogrowDefaults, self.settings.autogrow);
383
+ }
384
+ self._oldHeight = -1;
385
+ }
386
+
387
+ // If you put an absolute link as the path of any of the themes ignore the basePath
388
+ // preview theme
389
+ if (!self.settings.theme.preview.match(/^https?:\/\//)) {
390
+ self.settings.theme.preview = self.settings.basePath + self.settings.theme.preview;
391
+ }
392
+ // editor theme
393
+ if (!self.settings.theme.editor.match(/^https?:\/\//)) {
394
+ self.settings.theme.editor = self.settings.basePath + self.settings.theme.editor;
395
+ }
396
+ // base theme
397
+ if (!self.settings.theme.base.match(/^https?:\/\//)) {
398
+ self.settings.theme.base = self.settings.basePath + self.settings.theme.base;
399
+ }
400
+
401
+ // Grab the container element and save it to self.element
402
+ // if it's a string assume it's an ID and if it's an object
403
+ // assume it's a DOM element
404
+ if (typeof self.settings.container == 'string') {
405
+ self.element = document.getElementById(self.settings.container);
406
+ }
407
+ else if (typeof self.settings.container == 'object') {
408
+ self.element = self.settings.container;
409
+ }
410
+
411
+ // Figure out the file name. If no file name is given we'll use the ID.
412
+ // If there's no ID either we'll use a namespaced file name that's incremented
413
+ // based on the calling order. As long as it doesn't change, drafts will be saved.
414
+ if (!self.settings.file.name) {
415
+ if (typeof self.settings.container == 'string') {
416
+ self.settings.file.name = self.settings.container;
417
+ }
418
+ else if (typeof self.settings.container == 'object') {
419
+ if (self.element.id) {
420
+ self.settings.file.name = self.element.id;
421
+ }
422
+ else {
423
+ if (!EpicEditor._data.unnamedEditors) {
424
+ EpicEditor._data.unnamedEditors = [];
425
+ }
426
+ EpicEditor._data.unnamedEditors.push(self);
427
+ self.settings.file.name = '__epiceditor-untitled-' + EpicEditor._data.unnamedEditors.length;
428
+ }
429
+ }
430
+ }
431
+
432
+ if (self.settings.button.bar === "show") {
433
+ self.settings.button.bar = true;
434
+ }
435
+
436
+ if (self.settings.button.bar === "hide") {
437
+ self.settings.button.bar = false;
438
+ }
439
+
440
+ // Protect the id and overwrite if passed in as an option
441
+ // TODO: Put underscrore to denote that this is private
442
+ self._instanceId = 'epiceditor-' + Math.round(Math.random() * 100000);
443
+ self._storage = {};
444
+ self._canSave = true;
445
+
446
+ // Setup local storage of files
447
+ self._defaultFileSchema = function () {
448
+ return {
449
+ content: self.settings.file.defaultContent
450
+ , created: new Date()
451
+ , modified: new Date()
452
+ }
453
+ }
454
+
455
+ if (localStorage && self.settings.clientSideStorage) {
456
+ this._storage = localStorage;
457
+ if (this._storage[self.settings.localStorageName] && self.getFiles(self.settings.file.name) === undefined) {
458
+ _defaultFile = self._defaultFileSchema();
459
+ _defaultFile.content = self.settings.file.defaultContent;
460
+ }
461
+ }
462
+
463
+ if (!this._storage[self.settings.localStorageName]) {
464
+ defaultStorage = {};
465
+ defaultStorage[self.settings.file.name] = self._defaultFileSchema();
466
+ defaultStorage = JSON.stringify(defaultStorage);
467
+ this._storage[self.settings.localStorageName] = defaultStorage;
468
+ }
469
+
470
+ // A string to prepend files with to save draft versions of files
471
+ // and reset all preview drafts on each load!
472
+ self._previewDraftLocation = '__draft-';
473
+ self._storage[self._previewDraftLocation + self.settings.localStorageName] = self._storage[self.settings.localStorageName];
474
+
475
+ // This needs to replace the use of classes to check the state of EE
476
+ self._eeState = {
477
+ fullscreen: false
478
+ , preview: false
479
+ , edit: false
480
+ , loaded: false
481
+ , unloaded: false
482
+ }
483
+
484
+ // Now that it exists, allow binding of events if it doesn't exist yet
485
+ if (!self.events) {
486
+ self.events = {};
487
+ }
488
+
489
+ return this;
490
+ }
491
+
492
+ /**
493
+ * Inserts the EpicEditor into the DOM via an iframe and gets it ready for editing and previewing
494
+ * @returns {object} EpicEditor will be returned
495
+ */
496
+ EpicEditor.prototype.load = function (callback) {
497
+
498
+ // Get out early if it's already loaded
499
+ if (this.is('loaded')) { return this; }
500
+
501
+ // TODO: Gotta get the privates with underscores!
502
+ // TODO: Gotta document what these are for...
503
+ var self = this
504
+ , _HtmlTemplates
505
+ , iframeElement
506
+ , baseTag
507
+ , utilBtns
508
+ , utilBar
509
+ , utilBarTimer
510
+ , keypressTimer
511
+ , mousePos = { y: -1, x: -1 }
512
+ , _elementStates
513
+ , _isInEdit
514
+ , nativeFs = false
515
+ , nativeFsWebkit = false
516
+ , nativeFsMoz = false
517
+ , nativeFsW3C = false
518
+ , fsElement
519
+ , isMod = false
520
+ , isCtrl = false
521
+ , eventableIframes
522
+ , i // i is reused for loops
523
+ , boundAutogrow;
524
+
525
+ // Startup is a way to check if this EpicEditor is starting up. Useful for
526
+ // checking and doing certain things before EpicEditor emits a load event.
527
+ self._eeState.startup = true;
528
+
529
+ if (self.settings.useNativeFullscreen) {
530
+ nativeFsWebkit = document.body.webkitRequestFullScreen ? true : false;
531
+ nativeFsMoz = document.body.mozRequestFullScreen ? true : false;
532
+ nativeFsW3C = document.body.requestFullscreen ? true : false;
533
+ nativeFs = nativeFsWebkit || nativeFsMoz || nativeFsW3C;
534
+ }
535
+
536
+ // Fucking Safari's native fullscreen works terribly
537
+ // REMOVE THIS IF SAFARI 7 WORKS BETTER
538
+ if (_isSafari()) {
539
+ nativeFs = false;
540
+ nativeFsWebkit = false;
541
+ }
542
+
543
+ // It opens edit mode by default (for now);
544
+ if (!self.is('edit') && !self.is('preview')) {
545
+ self._eeState.edit = true;
546
+ }
547
+
548
+ callback = callback || function () {};
549
+
550
+ // The editor HTML
551
+ // TODO: edit-mode class should be dynamically added
552
+ _HtmlTemplates = {
553
+ // This is wrapping iframe element. It contains the other two iframes and the utilbar
554
+ chrome: '<div id="epiceditor-wrapper" class="epiceditor-edit-mode">' +
555
+ '<iframe frameborder="0" id="epiceditor-editor-frame"></iframe>' +
556
+ '<iframe frameborder="0" id="epiceditor-previewer-frame"></iframe>' +
557
+ '<div id="epiceditor-utilbar">' +
558
+ (self._previewEnabled ? '<button title="' + this.settings.string.togglePreview + '" class="epiceditor-toggle-btn epiceditor-toggle-preview-btn"></button> ' : '') +
559
+ (self._editEnabled ? '<button title="' + this.settings.string.toggleEdit + '" class="epiceditor-toggle-btn epiceditor-toggle-edit-btn"></button> ' : '') +
560
+ (self._fullscreenEnabled ? '<button title="' + this.settings.string.toggleFullscreen + '" class="epiceditor-fullscreen-btn"></button>' : '') +
561
+ '</div>' +
562
+ '</div>'
563
+
564
+ // The previewer is just an empty box for the generated HTML to go into
565
+ , previewer: '<div id="epiceditor-preview"></div>'
566
+ , editor: '<!doctype HTML>'
567
+ };
568
+
569
+ // Write an iframe and then select it for the editor
570
+ self.element.innerHTML = '<iframe scrolling="no" frameborder="0" id= "' + self._instanceId + '"></iframe>';
571
+
572
+ // Because browsers add things like invisible padding and margins and stuff
573
+ // to iframes, we need to set manually set the height so that the height
574
+ // doesn't keep increasing (by 2px?) every time reflow() is called.
575
+ // FIXME: Figure out how to fix this without setting this
576
+ self.element.style.height = self.element.offsetHeight + 'px';
577
+
578
+ iframeElement = document.getElementById(self._instanceId);
579
+
580
+ // Store a reference to the iframeElement itself
581
+ self.iframeElement = iframeElement;
582
+
583
+ // Grab the innards of the iframe (returns the document.body)
584
+ // TODO: Change self.iframe to self.iframeDocument
585
+ self.iframe = _getIframeInnards(iframeElement);
586
+ self.iframe.open();
587
+ self.iframe.write(_HtmlTemplates.chrome);
588
+
589
+ // Now that we got the innards of the iframe, we can grab the other iframes
590
+ self.editorIframe = self.iframe.getElementById('epiceditor-editor-frame')
591
+ self.previewerIframe = self.iframe.getElementById('epiceditor-previewer-frame');
592
+
593
+ // Setup the editor iframe
594
+ self.editorIframeDocument = _getIframeInnards(self.editorIframe);
595
+ self.editorIframeDocument.open();
596
+ // Need something for... you guessed it, Firefox
597
+ self.editorIframeDocument.write(_HtmlTemplates.editor);
598
+ self.editorIframeDocument.close();
599
+
600
+ // Setup the previewer iframe
601
+ self.previewerIframeDocument = _getIframeInnards(self.previewerIframe);
602
+ self.previewerIframeDocument.open();
603
+ self.previewerIframeDocument.write(_HtmlTemplates.previewer);
604
+
605
+ // Base tag is added so that links will open a new tab and not inside of the iframes
606
+ baseTag = self.previewerIframeDocument.createElement('base');
607
+ baseTag.target = '_blank';
608
+ self.previewerIframeDocument.getElementsByTagName('head')[0].appendChild(baseTag);
609
+
610
+ self.previewerIframeDocument.close();
611
+
612
+ self.reflow();
613
+
614
+ // Insert Base Stylesheet
615
+ _insertCSSLink(self.settings.theme.base, self.iframe, 'theme');
616
+
617
+ // Insert Editor Stylesheet
618
+ _insertCSSLink(self.settings.theme.editor, self.editorIframeDocument, 'theme');
619
+
620
+ // Insert Previewer Stylesheet
621
+ _insertCSSLink(self.settings.theme.preview, self.previewerIframeDocument, 'theme');
622
+
623
+ // Add a relative style to the overall wrapper to keep CSS relative to the editor
624
+ self.iframe.getElementById('epiceditor-wrapper').style.position = 'relative';
625
+
626
+ // Set the position to relative so we hide them with left: -999999px
627
+ self.editorIframe.style.position = 'absolute';
628
+ self.previewerIframe.style.position = 'absolute';
629
+
630
+ // Now grab the editor and previewer for later use
631
+ self.editor = self.editorIframeDocument.body;
632
+ self.previewer = self.previewerIframeDocument.getElementById('epiceditor-preview');
633
+
634
+ self.editor.contentEditable = true;
635
+
636
+ // Firefox's <body> gets all fucked up so, to be sure, we need to hardcode it
637
+ self.iframe.body.style.height = this.element.offsetHeight + 'px';
638
+
639
+ // Should actually check what mode it's in!
640
+ self.previewerIframe.style.left = '-999999px';
641
+
642
+ // Keep long lines from being longer than the editor
643
+ this.editorIframeDocument.body.style.wordWrap = 'break-word';
644
+
645
+ // FIXME figure out why it needs +2 px
646
+ if (_isIE() > -1) {
647
+ this.previewer.style.height = parseInt(_getStyle(this.previewer, 'height'), 10) + 2;
648
+ }
649
+
650
+ // If there is a file to be opened with that filename and it has content...
651
+ this.open(self.settings.file.name);
652
+
653
+ if (self.settings.focusOnLoad) {
654
+ // We need to wait until all three iframes are done loading by waiting until the parent
655
+ // iframe's ready state == complete, then we can focus on the contenteditable
656
+ self.iframe.addEventListener('readystatechange', function () {
657
+ if (self.iframe.readyState == 'complete') {
658
+ self.focus();
659
+ }
660
+ });
661
+ }
662
+
663
+ // Because IE scrolls the whole window to hash links, we need our own
664
+ // method of scrolling the iframe to an ID from clicking a hash
665
+ self.previewerIframeDocument.addEventListener('click', function (e) {
666
+ var el = e.target
667
+ , body = self.previewerIframeDocument.body;
668
+ if (el.nodeName == 'A') {
669
+ // Make sure the link is a hash and the link is local to the iframe
670
+ if (el.hash && el.hostname == window.location.hostname) {
671
+ // Prevent the whole window from scrolling
672
+ e.preventDefault();
673
+ // Prevent opening a new window
674
+ el.target = '_self';
675
+ // Scroll to the matching element, if an element exists
676
+ if (body.querySelector(el.hash)) {
677
+ body.scrollTop = body.querySelector(el.hash).offsetTop;
678
+ }
679
+ }
680
+ }
681
+ });
682
+
683
+ utilBtns = self.iframe.getElementById('epiceditor-utilbar');
684
+
685
+ // TODO: Move into fullscreen setup function (_setupFullscreen)
686
+ _elementStates = {}
687
+ self._goFullscreen = function (el) {
688
+ this._fixScrollbars('auto');
689
+
690
+ if (self.is('fullscreen')) {
691
+ self._exitFullscreen(el);
692
+ return;
693
+ }
694
+
695
+ if (nativeFs) {
696
+ if (nativeFsWebkit) {
697
+ el.webkitRequestFullScreen();
698
+ }
699
+ else if (nativeFsMoz) {
700
+ el.mozRequestFullScreen();
701
+ }
702
+ else if (nativeFsW3C) {
703
+ el.requestFullscreen();
704
+ }
705
+ }
706
+
707
+ _isInEdit = self.is('edit');
708
+
709
+ // Set the state of EE in fullscreen
710
+ // We set edit and preview to true also because they're visible
711
+ // we might want to allow fullscreen edit mode without preview (like a "zen" mode)
712
+ self._eeState.fullscreen = true;
713
+ self._eeState.edit = true;
714
+ self._eeState.preview = true;
715
+
716
+ // Cache calculations
717
+ var windowInnerWidth = window.innerWidth
718
+ , windowInnerHeight = window.innerHeight
719
+ , windowOuterWidth = window.outerWidth
720
+ , windowOuterHeight = window.outerHeight;
721
+
722
+ // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66)
723
+ if (!nativeFs) {
724
+ windowOuterHeight = window.innerHeight;
725
+ }
726
+
727
+ // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper
728
+ // the editor's width wont be the same as before
729
+ _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', {
730
+ 'width': windowOuterWidth / 2 + 'px'
731
+ , 'height': windowOuterHeight + 'px'
732
+ , 'float': 'left' // Most browsers
733
+ , 'cssFloat': 'left' // FF
734
+ , 'styleFloat': 'left' // Older IEs
735
+ , 'display': 'block'
736
+ , 'position': 'static'
737
+ , 'left': ''
738
+ });
739
+
740
+ // the previewer
741
+ _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', {
742
+ 'width': windowOuterWidth / 2 + 'px'
743
+ , 'height': windowOuterHeight + 'px'
744
+ , 'float': 'right' // Most browsers
745
+ , 'cssFloat': 'right' // FF
746
+ , 'styleFloat': 'right' // Older IEs
747
+ , 'display': 'block'
748
+ , 'position': 'static'
749
+ , 'left': ''
750
+ });
751
+
752
+ // Setup the containing element CSS for fullscreen
753
+ _elementStates.element = _saveStyleState(self.element, 'save', {
754
+ 'position': 'fixed'
755
+ , 'top': '0'
756
+ , 'left': '0'
757
+ , 'width': '100%'
758
+ , 'z-index': '9999' // Most browsers
759
+ , 'zIndex': '9999' // Firefox
760
+ , 'border': 'none'
761
+ , 'margin': '0'
762
+ // Should use the base styles background!
763
+ , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below
764
+ , 'height': windowInnerHeight + 'px'
765
+ });
766
+
767
+ // The iframe element
768
+ _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', {
769
+ 'width': windowOuterWidth + 'px'
770
+ , 'height': windowInnerHeight + 'px'
771
+ });
772
+
773
+ // ...Oh, and hide the buttons and prevent scrolling
774
+ utilBtns.style.visibility = 'hidden';
775
+
776
+ if (!nativeFs) {
777
+ document.body.style.overflow = 'hidden';
778
+ }
779
+
780
+ self.preview();
781
+
782
+ self.focus();
783
+
784
+ self.emit('fullscreenenter');
785
+ };
786
+
787
+ self._exitFullscreen = function (el) {
788
+ this._fixScrollbars();
789
+
790
+ _saveStyleState(self.element, 'apply', _elementStates.element);
791
+ _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement);
792
+ _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe);
793
+ _saveStyleState(self.previewerIframe, 'apply', _elementStates.previewerIframe);
794
+
795
+ // We want to always revert back to the original styles in the CSS so,
796
+ // if it's a fluid width container it will expand on resize and not get
797
+ // stuck at a specific width after closing fullscreen.
798
+ self.element.style.width = self._eeState.reflowWidth ? self._eeState.reflowWidth : '';
799
+ self.element.style.height = self._eeState.reflowHeight ? self._eeState.reflowHeight : '';
800
+
801
+ utilBtns.style.visibility = 'visible';
802
+
803
+ // Put the editor back in the right state
804
+ // TODO: This is ugly... how do we make this nicer?
805
+ // setting fullscreen to false here prevents the
806
+ // native fs callback from calling this function again
807
+ self._eeState.fullscreen = false;
808
+
809
+ if (!nativeFs) {
810
+ document.body.style.overflow = 'auto';
811
+ }
812
+ else {
813
+ if (nativeFsWebkit) {
814
+ document.webkitCancelFullScreen();
815
+ }
816
+ else if (nativeFsMoz) {
817
+ document.mozCancelFullScreen();
818
+ }
819
+ else if (nativeFsW3C) {
820
+ document.exitFullscreen();
821
+ }
822
+ }
823
+
824
+ if (_isInEdit) {
825
+ self.edit();
826
+ }
827
+ else {
828
+ self.preview();
829
+ }
830
+
831
+ self.reflow();
832
+
833
+ self.emit('fullscreenexit');
834
+ };
835
+
836
+ // This setups up live previews by triggering preview() IF in fullscreen on keyup
837
+ self.editor.addEventListener('keyup', function () {
838
+ if (keypressTimer) {
839
+ window.clearTimeout(keypressTimer);
840
+ }
841
+ keypressTimer = window.setTimeout(function () {
842
+ if (self.is('fullscreen')) {
843
+ self.preview();
844
+ }
845
+ }, 250);
846
+ });
847
+
848
+ fsElement = self.iframeElement;
849
+
850
+ // Sets up the onclick event on utility buttons
851
+ utilBtns.addEventListener('click', function (e) {
852
+ var targetClass = e.target.className;
853
+ if (targetClass.indexOf('epiceditor-toggle-preview-btn') > -1) {
854
+ self.preview();
855
+ }
856
+ else if (targetClass.indexOf('epiceditor-toggle-edit-btn') > -1) {
857
+ self.edit();
858
+ }
859
+ else if (targetClass.indexOf('epiceditor-fullscreen-btn') > -1) {
860
+ self._goFullscreen(fsElement);
861
+ }
862
+ });
863
+
864
+ // Sets up the NATIVE fullscreen editor/previewer for WebKit
865
+ if (nativeFsWebkit) {
866
+ document.addEventListener('webkitfullscreenchange', function () {
867
+ if (!document.webkitIsFullScreen && self._eeState.fullscreen) {
868
+ self._exitFullscreen(fsElement);
869
+ }
870
+ }, false);
871
+ }
872
+ else if (nativeFsMoz) {
873
+ document.addEventListener('mozfullscreenchange', function () {
874
+ if (!document.mozFullScreen && self._eeState.fullscreen) {
875
+ self._exitFullscreen(fsElement);
876
+ }
877
+ }, false);
878
+ }
879
+ else if (nativeFsW3C) {
880
+ document.addEventListener('fullscreenchange', function () {
881
+ if (document.fullscreenElement == null && self._eeState.fullscreen) {
882
+ self._exitFullscreen(fsElement);
883
+ }
884
+ }, false);
885
+ }
886
+
887
+ // TODO: Move utilBar stuff into a utilBar setup function (_setupUtilBar)
888
+ utilBar = self.iframe.getElementById('epiceditor-utilbar');
889
+
890
+ // Hide it at first until they move their mouse
891
+ if (self.settings.button.bar !== true) {
892
+ utilBar.style.display = 'none';
893
+ }
894
+
895
+ utilBar.addEventListener('mouseover', function () {
896
+ if (utilBarTimer) {
897
+ clearTimeout(utilBarTimer);
898
+ }
899
+ });
900
+
901
+ function utilBarHandler(e) {
902
+ if (self.settings.button.bar !== "auto") {
903
+ return;
904
+ }
905
+ // Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code
906
+ // we do this for 2 reasons:
907
+ // 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
908
+ // a mousemove of a few pixels depending on how hard you scroll
909
+ // 2. We give a slight buffer to the user in case he barely touches his touchpad or mouse and not trigger the UI
910
+ if (Math.abs(mousePos.y - e.pageY) >= 5 || Math.abs(mousePos.x - e.pageX) >= 5) {
911
+ utilBar.style.display = 'block';
912
+ // if we have a timer already running, kill it out
913
+ if (utilBarTimer) {
914
+ clearTimeout(utilBarTimer);
915
+ }
916
+
917
+ // begin a new timer that hides our object after 1000 ms
918
+ utilBarTimer = window.setTimeout(function () {
919
+ utilBar.style.display = 'none';
920
+ }, 1000);
921
+ }
922
+ mousePos = { y: e.pageY, x: e.pageX };
923
+ }
924
+
925
+ // Add keyboard shortcuts for convenience.
926
+ function shortcutHandler(e) {
927
+ if (e.keyCode == self.settings.shortcut.modifier) { isMod = true } // check for modifier press(default is alt key), save to var
928
+ if (e.keyCode == 17) { isCtrl = true } // check for ctrl/cmnd press, in order to catch ctrl/cmnd + s
929
+
930
+ // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview
931
+ if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) {
932
+ e.preventDefault();
933
+ if (self.is('edit') && self._previewEnabled) {
934
+ self.preview();
935
+ }
936
+ else if (self._editEnabled) {
937
+ self.edit();
938
+ }
939
+ }
940
+ // Check for alt+f - default shortcut to make editor fullscreen
941
+ if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen && self._fullscreenEnabled) {
942
+ e.preventDefault();
943
+ self._goFullscreen(fsElement);
944
+ }
945
+
946
+ // Set the modifier key to false once *any* key combo is completed
947
+ // or else, on Windows, hitting the alt key will lock the isMod state to true (ticket #133)
948
+ if (isMod === true && e.keyCode !== self.settings.shortcut.modifier) {
949
+ isMod = false;
950
+ }
951
+
952
+ // When a user presses "esc", revert everything!
953
+ if (e.keyCode == 27 && self.is('fullscreen')) {
954
+ self._exitFullscreen(fsElement);
955
+ }
956
+
957
+ // Check for ctrl + s (since a lot of people do it out of habit) and make it do nothing
958
+ if (isCtrl === true && e.keyCode == 83) {
959
+ self.save();
960
+ e.preventDefault();
961
+ isCtrl = false;
962
+ }
963
+
964
+ // Do the same for Mac now (metaKey == cmd).
965
+ if (e.metaKey && e.keyCode == 83) {
966
+ self.save();
967
+ e.preventDefault();
968
+ }
969
+
970
+ }
971
+
972
+ function shortcutUpHandler(e) {
973
+ if (e.keyCode == self.settings.shortcut.modifier) { isMod = false }
974
+ if (e.keyCode == 17) { isCtrl = false }
975
+ }
976
+
977
+ function pasteHandler(e) {
978
+ var content;
979
+ if (e.clipboardData) {
980
+ //FF 22, Webkit, "standards"
981
+ e.preventDefault();
982
+ content = e.clipboardData.getData("text/plain");
983
+ self.editorIframeDocument.execCommand("insertText", false, content);
984
+ }
985
+ else if (window.clipboardData) {
986
+ //IE, "nasty"
987
+ e.preventDefault();
988
+ content = window.clipboardData.getData("Text");
989
+ content = content.replace(/</g, '&lt;');
990
+ content = content.replace(/>/g, '&gt;');
991
+ content = content.replace(/\n/g, '<br>');
992
+ content = content.replace(/\r/g, ''); //fuck you, ie!
993
+ content = content.replace(/<br>\s/g, '<br>&nbsp;')
994
+ content = content.replace(/\s\s\s/g, '&nbsp; &nbsp;')
995
+ content = content.replace(/\s\s/g, '&nbsp; ')
996
+ self.editorIframeDocument.selection.createRange().pasteHTML(content);
997
+ }
998
+ }
999
+
1000
+ // Hide and show the util bar based on mouse movements
1001
+ eventableIframes = [self.previewerIframeDocument, self.editorIframeDocument];
1002
+
1003
+ for (i = 0; i < eventableIframes.length; i++) {
1004
+ eventableIframes[i].addEventListener('mousemove', function (e) {
1005
+ utilBarHandler(e);
1006
+ });
1007
+ eventableIframes[i].addEventListener('scroll', function (e) {
1008
+ utilBarHandler(e);
1009
+ });
1010
+ eventableIframes[i].addEventListener('keyup', function (e) {
1011
+ shortcutUpHandler(e);
1012
+ });
1013
+ eventableIframes[i].addEventListener('keydown', function (e) {
1014
+ shortcutHandler(e);
1015
+ });
1016
+ eventableIframes[i].addEventListener('paste', function (e) {
1017
+ pasteHandler(e);
1018
+ });
1019
+ }
1020
+
1021
+ // Save the document every 100ms by default
1022
+ // TODO: Move into autosave setup function (_setupAutoSave)
1023
+ if (self.settings.file.autoSave) {
1024
+ self._saveIntervalTimer = window.setInterval(function () {
1025
+ if (!self._canSave) {
1026
+ return;
1027
+ }
1028
+ self.save(false, true);
1029
+ }, self.settings.file.autoSave);
1030
+ }
1031
+
1032
+ // Update a textarea automatically if a textarea is given so you don't need
1033
+ // AJAX to submit a form and instead fall back to normal form behavior
1034
+ if (self.settings.textarea) {
1035
+ self._setupTextareaSync();
1036
+ }
1037
+
1038
+ window.addEventListener('resize', function () {
1039
+ // If NOT webkit, and in fullscreen, we need to account for browser resizing
1040
+ // we don't care about webkit because you can't resize in webkit's fullscreen
1041
+ if (self.is('fullscreen')) {
1042
+ _applyStyles(self.iframeElement, {
1043
+ 'width': window.outerWidth + 'px'
1044
+ , 'height': window.innerHeight + 'px'
1045
+ });
1046
+
1047
+ _applyStyles(self.element, {
1048
+ 'height': window.innerHeight + 'px'
1049
+ });
1050
+
1051
+ _applyStyles(self.previewerIframe, {
1052
+ 'width': window.outerWidth / 2 + 'px'
1053
+ , 'height': window.innerHeight + 'px'
1054
+ });
1055
+
1056
+ _applyStyles(self.editorIframe, {
1057
+ 'width': window.outerWidth / 2 + 'px'
1058
+ , 'height': window.innerHeight + 'px'
1059
+ });
1060
+ }
1061
+ // Makes the editor support fluid width when not in fullscreen mode
1062
+ else if (!self.is('fullscreen')) {
1063
+ self.reflow();
1064
+ }
1065
+ });
1066
+
1067
+ // Set states before flipping edit and preview modes
1068
+ self._eeState.loaded = true;
1069
+ self._eeState.unloaded = false;
1070
+
1071
+ if (self.is('preview')) {
1072
+ self.preview();
1073
+ }
1074
+ else {
1075
+ self.edit();
1076
+ }
1077
+
1078
+ self.iframe.close();
1079
+ self._eeState.startup = false;
1080
+
1081
+ if (self.settings.autogrow) {
1082
+ self._fixScrollbars();
1083
+
1084
+ boundAutogrow = function () {
1085
+ setTimeout(function () {
1086
+ self._autogrow();
1087
+ }, 1);
1088
+ };
1089
+
1090
+ //for if autosave is disabled or very slow
1091
+ ['keydown', 'keyup', 'paste', 'cut'].forEach(function (ev) {
1092
+ self.getElement('editor').addEventListener(ev, boundAutogrow);
1093
+ });
1094
+
1095
+ self.on('__update', boundAutogrow);
1096
+ self.on('edit', function () {
1097
+ setTimeout(boundAutogrow, 50)
1098
+ });
1099
+ self.on('preview', function () {
1100
+ setTimeout(boundAutogrow, 50)
1101
+ });
1102
+
1103
+ //for browsers that have rendering delays
1104
+ setTimeout(boundAutogrow, 50);
1105
+ boundAutogrow();
1106
+ }
1107
+
1108
+ // The callback and call are the same thing, but different ways to access them
1109
+ callback.call(this);
1110
+ this.emit('load');
1111
+ return this;
1112
+ }
1113
+
1114
+ EpicEditor.prototype._setupTextareaSync = function () {
1115
+ var self = this
1116
+ , textareaFileName = self.settings.file.name
1117
+ , _syncTextarea;
1118
+
1119
+ // Even if autoSave is false, we want to make sure to keep the textarea synced
1120
+ // with the editor's content. One bad thing about this tho is that we're
1121
+ // creating two timers now in some configurations. We keep the textarea synced
1122
+ // by saving and opening the textarea content from the draft file storage.
1123
+ self._textareaSaveTimer = window.setInterval(function () {
1124
+ if (!self._canSave) {
1125
+ return;
1126
+ }
1127
+ self.save(true);
1128
+ }, 100);
1129
+
1130
+ _syncTextarea = function () {
1131
+ // TODO: Figure out root cause for having to do this ||.
1132
+ // This only happens for draft files. Probably has something to do with
1133
+ // the fact draft files haven't been saved by the time this is called.
1134
+ // TODO: Add test for this case.
1135
+ self._textareaElement.value = self.exportFile(textareaFileName, 'text', true) || self.settings.file.defaultContent;
1136
+ }
1137
+
1138
+ if (typeof self.settings.textarea == 'string') {
1139
+ self._textareaElement = document.getElementById(self.settings.textarea);
1140
+ }
1141
+ else if (typeof self.settings.textarea == 'object') {
1142
+ self._textareaElement = self.settings.textarea;
1143
+ }
1144
+
1145
+ // On page load, if there's content in the textarea that means one of two
1146
+ // different things:
1147
+ //
1148
+ // 1. The editor didn't load and the user was writing in the textarea and
1149
+ // now he refreshed the page or the JS loaded and the textarea now has
1150
+ // content. If this is the case the user probably expects his content is
1151
+ // moved into the editor and not lose what he typed.
1152
+ //
1153
+ // 2. The developer put content in the textarea from some server side
1154
+ // code. In this case, the textarea will take precedence.
1155
+ //
1156
+ // If the developer wants drafts to be recoverable they should check if
1157
+ // the local file in localStorage's modified date is newer than the server.
1158
+ if (self._textareaElement.value !== '') {
1159
+ self.importFile(textareaFileName, self._textareaElement.value);
1160
+
1161
+ // manually save draft after import so there is no delay between the
1162
+ // import and exporting in _syncTextarea. Without this, _syncTextarea
1163
+ // will pull the saved data from localStorage which will be <=100ms old.
1164
+ self.save(true);
1165
+ }
1166
+
1167
+ // Update the textarea on load and pull from drafts
1168
+ _syncTextarea();
1169
+
1170
+ // Make sure to keep it updated
1171
+ self.on('__update', _syncTextarea);
1172
+ }
1173
+
1174
+ /**
1175
+ * Will NOT focus the editor if the editor is still starting up AND
1176
+ * focusOnLoad is set to false. This allows you to place this in code that
1177
+ * gets fired during .load() without worrying about it overriding the user's
1178
+ * option. For example use cases see preview() and edit().
1179
+ * @returns {undefined}
1180
+ */
1181
+
1182
+ // Prevent focus when the user sets focusOnLoad to false by checking if the
1183
+ // editor is starting up AND if focusOnLoad is true
1184
+ EpicEditor.prototype._focusExceptOnLoad = function () {
1185
+ var self = this;
1186
+ if ((self._eeState.startup && self.settings.focusOnLoad) || !self._eeState.startup) {
1187
+ self.focus();
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * Will remove the editor, but not offline files
1193
+ * @returns {object} EpicEditor will be returned
1194
+ */
1195
+ EpicEditor.prototype.unload = function (callback) {
1196
+
1197
+ // Make sure the editor isn't already unloaded.
1198
+ if (this.is('unloaded')) {
1199
+ throw new Error('Editor isn\'t loaded');
1200
+ }
1201
+
1202
+ var self = this
1203
+ , editor = window.parent.document.getElementById(self._instanceId);
1204
+
1205
+ editor.parentNode.removeChild(editor);
1206
+ self._eeState.loaded = false;
1207
+ self._eeState.unloaded = true;
1208
+ callback = callback || function () {};
1209
+
1210
+ if (self.settings.textarea) {
1211
+ self._textareaElement.value = "";
1212
+ self.removeListener('__update');
1213
+ }
1214
+
1215
+ if (self._saveIntervalTimer) {
1216
+ window.clearInterval(self._saveIntervalTimer);
1217
+ }
1218
+ if (self._textareaSaveTimer) {
1219
+ window.clearInterval(self._textareaSaveTimer);
1220
+ }
1221
+
1222
+ callback.call(this);
1223
+ self.emit('unload');
1224
+ return self;
1225
+ }
1226
+
1227
+ /**
1228
+ * reflow allows you to dynamically re-fit the editor in the parent without
1229
+ * having to unload and then reload the editor again.
1230
+ *
1231
+ * reflow will also emit a `reflow` event and will return the new dimensions.
1232
+ * If it's called without params it'll return the new width and height and if
1233
+ * it's called with just width or just height it'll just return the width or
1234
+ * height. It's returned as an object like: { width: '100px', height: '1px' }
1235
+ *
1236
+ * @param {string|null} kind Can either be 'width' or 'height' or null
1237
+ * if null, both the height and width will be resized
1238
+ * @param {function} callback A function to fire after the reflow is finished.
1239
+ * Will return the width / height in an obj as the first param of the callback.
1240
+ * @returns {object} EpicEditor will be returned
1241
+ */
1242
+ EpicEditor.prototype.reflow = function (kind, callback) {
1243
+ var self = this
1244
+ , widthDiff = _outerWidth(self.element) - self.element.offsetWidth
1245
+ , heightDiff = _outerHeight(self.element) - self.element.offsetHeight
1246
+ , elements = [self.iframeElement, self.editorIframe, self.previewerIframe]
1247
+ , eventData = {}
1248
+ , newWidth
1249
+ , newHeight;
1250
+
1251
+ if (typeof kind == 'function') {
1252
+ callback = kind;
1253
+ kind = null;
1254
+ }
1255
+
1256
+ if (!callback) {
1257
+ callback = function () {};
1258
+ }
1259
+
1260
+ for (var x = 0; x < elements.length; x++) {
1261
+ if (!kind || kind == 'width') {
1262
+ newWidth = self.element.offsetWidth - widthDiff + 'px';
1263
+ elements[x].style.width = newWidth;
1264
+ self._eeState.reflowWidth = newWidth;
1265
+ eventData.width = newWidth;
1266
+ }
1267
+ if (!kind || kind == 'height') {
1268
+ newHeight = self.element.offsetHeight - heightDiff + 'px';
1269
+ elements[x].style.height = newHeight;
1270
+ self._eeState.reflowHeight = newHeight
1271
+ eventData.height = newHeight;
1272
+ }
1273
+ }
1274
+
1275
+ self.emit('reflow', eventData);
1276
+ callback.call(this, eventData);
1277
+ return self;
1278
+ }
1279
+
1280
+ /**
1281
+ * Will take the markdown and generate a preview view based on the theme
1282
+ * @returns {object} EpicEditor will be returned
1283
+ */
1284
+ EpicEditor.prototype.preview = function () {
1285
+ var self = this
1286
+ , x
1287
+ , theme = self.settings.theme.preview
1288
+ , anchors;
1289
+
1290
+ _replaceClass(self.getElement('wrapper'), 'epiceditor-edit-mode', 'epiceditor-preview-mode');
1291
+
1292
+ // Check if no CSS theme link exists
1293
+ if (!self.previewerIframeDocument.getElementById('theme')) {
1294
+ _insertCSSLink(theme, self.previewerIframeDocument, 'theme');
1295
+ }
1296
+ else if (self.previewerIframeDocument.getElementById('theme').name !== theme) {
1297
+ self.previewerIframeDocument.getElementById('theme').href = theme;
1298
+ }
1299
+
1300
+ // Save a preview draft since it might not be saved to the real file yet
1301
+ self.save(true);
1302
+
1303
+ // Add the generated draft HTML into the previewer
1304
+ self.previewer.innerHTML = self.exportFile(null, 'html', true);
1305
+
1306
+ // Hide the editor and display the previewer
1307
+ if (!self.is('fullscreen')) {
1308
+ self.editorIframe.style.left = '-999999px';
1309
+ self.previewerIframe.style.left = '';
1310
+ self._eeState.preview = true;
1311
+ self._eeState.edit = false;
1312
+ self._focusExceptOnLoad();
1313
+ }
1314
+
1315
+ self.emit('preview');
1316
+ return self;
1317
+ }
1318
+
1319
+ /**
1320
+ * Helper to focus on the editor iframe. Will figure out which iframe to
1321
+ * focus on based on which one is active and will handle the cross browser
1322
+ * issues with focusing on the iframe vs the document body.
1323
+ * @returns {object} EpicEditor will be returned
1324
+ */
1325
+ EpicEditor.prototype.focus = function (pageload) {
1326
+ var self = this
1327
+ , isPreview = self.is('preview')
1328
+ , focusElement = isPreview ? self.previewerIframeDocument.body
1329
+ : self.editorIframeDocument.body;
1330
+
1331
+ if (_isFirefox() && isPreview) {
1332
+ focusElement = self.previewerIframe;
1333
+ }
1334
+
1335
+ focusElement.focus();
1336
+ return this;
1337
+ }
1338
+
1339
+ /**
1340
+ * Puts the editor into fullscreen mode
1341
+ * @returns {object} EpicEditor will be returned
1342
+ */
1343
+ EpicEditor.prototype.enterFullscreen = function () {
1344
+ if (this.is('fullscreen')) { return this; }
1345
+ this._goFullscreen(this.iframeElement);
1346
+ return this;
1347
+ }
1348
+
1349
+ /**
1350
+ * Closes fullscreen mode if opened
1351
+ * @returns {object} EpicEditor will be returned
1352
+ */
1353
+ EpicEditor.prototype.exitFullscreen = function () {
1354
+ if (!this.is('fullscreen')) { return this; }
1355
+ this._exitFullscreen(this.iframeElement);
1356
+ return this;
1357
+ }
1358
+
1359
+ /**
1360
+ * Hides the preview and shows the editor again
1361
+ * @returns {object} EpicEditor will be returned
1362
+ */
1363
+ EpicEditor.prototype.edit = function () {
1364
+ var self = this;
1365
+ _replaceClass(self.getElement('wrapper'), 'epiceditor-preview-mode', 'epiceditor-edit-mode');
1366
+ self._eeState.preview = false;
1367
+ self._eeState.edit = true;
1368
+ self.editorIframe.style.left = '';
1369
+ self.previewerIframe.style.left = '-999999px';
1370
+ self._focusExceptOnLoad();
1371
+ self.emit('edit');
1372
+ return this;
1373
+ }
1374
+
1375
+ /**
1376
+ * Grabs a specificed HTML node. Use it as a shortcut to getting the iframe contents
1377
+ * @param {String} name The name of the node (can be document, body, editor, previewer, or wrapper)
1378
+ * @returns {Object|Null}
1379
+ */
1380
+ EpicEditor.prototype.getElement = function (name) {
1381
+ var available = {
1382
+ "container": this.element
1383
+ , "wrapper": this.iframe.getElementById('epiceditor-wrapper')
1384
+ , "wrapperIframe": this.iframeElement
1385
+ , "editor": this.editorIframeDocument
1386
+ , "editorIframe": this.editorIframe
1387
+ , "previewer": this.previewerIframeDocument
1388
+ , "previewerIframe": this.previewerIframe
1389
+ }
1390
+
1391
+ // Check that the given string is a possible option and verify the editor isn't unloaded
1392
+ // without this, you'd be given a reference to an object that no longer exists in the DOM
1393
+ if (!available[name] || this.is('unloaded')) {
1394
+ return null;
1395
+ }
1396
+ else {
1397
+ return available[name];
1398
+ }
1399
+ }
1400
+
1401
+ /**
1402
+ * Returns a boolean of each "state" of the editor. For example "editor.is('loaded')" // returns true/false
1403
+ * @param {String} what the state you want to check for
1404
+ * @returns {Boolean}
1405
+ */
1406
+ EpicEditor.prototype.is = function (what) {
1407
+ var self = this;
1408
+ switch (what) {
1409
+ case 'loaded':
1410
+ return self._eeState.loaded;
1411
+ case 'unloaded':
1412
+ return self._eeState.unloaded
1413
+ case 'preview':
1414
+ return self._eeState.preview
1415
+ case 'edit':
1416
+ return self._eeState.edit;
1417
+ case 'fullscreen':
1418
+ return self._eeState.fullscreen;
1419
+ // TODO: This "works", but the tests are saying otherwise. Come back to this
1420
+ // and figure out how to fix it.
1421
+ // case 'focused':
1422
+ // return document.activeElement == self.iframeElement;
1423
+ default:
1424
+ return false;
1425
+ }
1426
+ }
1427
+
1428
+ /**
1429
+ * Opens a file
1430
+ * @param {string} name The name of the file you want to open
1431
+ * @returns {object} EpicEditor will be returned
1432
+ */
1433
+ EpicEditor.prototype.open = function (name) {
1434
+ var self = this
1435
+ , defaultContent = self.settings.file.defaultContent
1436
+ , fileObj;
1437
+ name = name || self.settings.file.name;
1438
+ self.settings.file.name = name;
1439
+ if (this._storage[self.settings.localStorageName]) {
1440
+ fileObj = self.exportFile(name);
1441
+ if (fileObj !== undefined) {
1442
+ _setText(self.editor, fileObj);
1443
+ self.emit('read');
1444
+ }
1445
+ else {
1446
+ _setText(self.editor, defaultContent);
1447
+ self.save(); // ensure a save
1448
+ self.emit('create');
1449
+ }
1450
+ self.previewer.innerHTML = self.exportFile(null, 'html');
1451
+ self.emit('open');
1452
+ }
1453
+ return this;
1454
+ }
1455
+
1456
+ /**
1457
+ * Saves content for offline use
1458
+ * @returns {object} EpicEditor will be returned
1459
+ */
1460
+ EpicEditor.prototype.save = function (_isPreviewDraft, _isAuto) {
1461
+ var self = this
1462
+ , storage
1463
+ , isUpdate = false
1464
+ , file = self.settings.file.name
1465
+ , previewDraftName = ''
1466
+ , data = this._storage[previewDraftName + self.settings.localStorageName]
1467
+ , content = _getText(this.editor);
1468
+
1469
+ if (_isPreviewDraft) {
1470
+ previewDraftName = self._previewDraftLocation;
1471
+ }
1472
+
1473
+ // This could have been false but since we're manually saving
1474
+ // we know it's save to start autoSaving again
1475
+ this._canSave = true;
1476
+
1477
+ // Guard against storage being wiped out without EpicEditor knowing
1478
+ // TODO: Emit saving error - storage seems to have been wiped
1479
+ if (data) {
1480
+ storage = JSON.parse(this._storage[previewDraftName + self.settings.localStorageName]);
1481
+
1482
+ // If the file doesn't exist we need to create it
1483
+ if (storage[file] === undefined) {
1484
+ storage[file] = self._defaultFileSchema();
1485
+ }
1486
+
1487
+ // If it does, we need to check if the content is different and
1488
+ // if it is, send the update event and update the timestamp
1489
+ else if (content !== storage[file].content) {
1490
+ storage[file].modified = new Date();
1491
+ isUpdate = true;
1492
+ }
1493
+ //don't bother autosaving if the content hasn't actually changed
1494
+ else if (_isAuto) {
1495
+ return;
1496
+ }
1497
+
1498
+ storage[file].content = content;
1499
+ this._storage[previewDraftName + self.settings.localStorageName] = JSON.stringify(storage);
1500
+
1501
+ // After the content is actually changed, emit update so it emits the updated content
1502
+ if (isUpdate) {
1503
+ self.emit('update');
1504
+ // Emit a private update event so it can't get accidentally removed
1505
+ self.emit('__update');
1506
+ }
1507
+
1508
+ if (_isAuto) {
1509
+ this.emit('autosave');
1510
+ }
1511
+ else if (!_isPreviewDraft) {
1512
+ this.emit('save');
1513
+ }
1514
+ }
1515
+
1516
+ return this;
1517
+ }
1518
+
1519
+ /**
1520
+ * Removes a page
1521
+ * @param {string} name The name of the file you want to remove from localStorage
1522
+ * @returns {object} EpicEditor will be returned
1523
+ */
1524
+ EpicEditor.prototype.remove = function (name) {
1525
+ var self = this
1526
+ , s;
1527
+ name = name || self.settings.file.name;
1528
+
1529
+ // If you're trying to delete a page you have open, block saving
1530
+ if (name == self.settings.file.name) {
1531
+ self._canSave = false;
1532
+ }
1533
+
1534
+ s = JSON.parse(this._storage[self.settings.localStorageName]);
1535
+ delete s[name];
1536
+ this._storage[self.settings.localStorageName] = JSON.stringify(s);
1537
+ this.emit('remove');
1538
+ return this;
1539
+ };
1540
+
1541
+ /**
1542
+ * Renames a file
1543
+ * @param {string} oldName The old file name
1544
+ * @param {string} newName The new file name
1545
+ * @returns {object} EpicEditor will be returned
1546
+ */
1547
+ EpicEditor.prototype.rename = function (oldName, newName) {
1548
+ var self = this
1549
+ , s = JSON.parse(this._storage[self.settings.localStorageName]);
1550
+ s[newName] = s[oldName];
1551
+ delete s[oldName];
1552
+ this._storage[self.settings.localStorageName] = JSON.stringify(s);
1553
+ self.open(newName);
1554
+ return this;
1555
+ };
1556
+
1557
+ /**
1558
+ * Imports a file and it's contents and opens it
1559
+ * @param {string} name The name of the file you want to import (will overwrite existing files!)
1560
+ * @param {string} content Content of the file you want to import
1561
+ * @param {string} kind The kind of file you want to import (TBI)
1562
+ * @param {object} meta Meta data you want to save with your file.
1563
+ * @returns {object} EpicEditor will be returned
1564
+ */
1565
+ EpicEditor.prototype.importFile = function (name, content, kind, meta) {
1566
+ var self = this
1567
+ , isNew = false;
1568
+
1569
+ name = name || self.settings.file.name;
1570
+ content = content || '';
1571
+ kind = kind || 'md';
1572
+ meta = meta || {};
1573
+
1574
+ if (JSON.parse(this._storage[self.settings.localStorageName])[name] === undefined) {
1575
+ isNew = true;
1576
+ }
1577
+
1578
+ // Set our current file to the new file and update the content
1579
+ self.settings.file.name = name;
1580
+ _setText(self.editor, content);
1581
+
1582
+ if (isNew) {
1583
+ self.emit('create');
1584
+ }
1585
+
1586
+ self.save();
1587
+
1588
+ if (self.is('fullscreen')) {
1589
+ self.preview();
1590
+ }
1591
+
1592
+ //firefox has trouble with importing and working out the size right away
1593
+ if (self.settings.autogrow) {
1594
+ setTimeout(function () {
1595
+ self._autogrow();
1596
+ }, 50);
1597
+ }
1598
+
1599
+ return this;
1600
+ };
1601
+
1602
+ /**
1603
+ * Gets the local filestore
1604
+ * @param {string} name Name of the file in the store
1605
+ * @returns {object|undefined} the local filestore, or a specific file in the store, if a name is given
1606
+ */
1607
+ EpicEditor.prototype._getFileStore = function (name, _isPreviewDraft) {
1608
+ var previewDraftName = ''
1609
+ , store;
1610
+ if (_isPreviewDraft) {
1611
+ previewDraftName = this._previewDraftLocation;
1612
+ }
1613
+ store = JSON.parse(this._storage[previewDraftName + this.settings.localStorageName]);
1614
+ if (name) {
1615
+ return store[name];
1616
+ }
1617
+ else {
1618
+ return store;
1619
+ }
1620
+ }
1621
+
1622
+ /**
1623
+ * Exports a file as a string in a supported format
1624
+ * @param {string} name Name of the file you want to export (case sensitive)
1625
+ * @param {string} kind Kind of file you want the content in (currently supports html and text, default is the format the browser "wants")
1626
+ * @returns {string|undefined} The content of the file in the content given or undefined if it doesn't exist
1627
+ */
1628
+ EpicEditor.prototype.exportFile = function (name, kind, _isPreviewDraft) {
1629
+ var self = this
1630
+ , file
1631
+ , content;
1632
+
1633
+ name = name || self.settings.file.name;
1634
+ kind = kind || 'text';
1635
+
1636
+ file = self._getFileStore(name, _isPreviewDraft);
1637
+
1638
+ // If the file doesn't exist just return early with undefined
1639
+ if (file === undefined) {
1640
+ return;
1641
+ }
1642
+
1643
+ content = file.content;
1644
+
1645
+ switch (kind) {
1646
+ case 'html':
1647
+ content = _sanitizeRawContent(content);
1648
+ return self.settings.parser(content);
1649
+ case 'text':
1650
+ return _sanitizeRawContent(content);
1651
+ case 'json':
1652
+ file.content = _sanitizeRawContent(file.content);
1653
+ return JSON.stringify(file);
1654
+ case 'raw':
1655
+ return content;
1656
+ default:
1657
+ return content;
1658
+ }
1659
+ }
1660
+
1661
+ /**
1662
+ * Gets the contents and metadata for files
1663
+ * @param {string} name Name of the file whose data you want (case sensitive)
1664
+ * @param {boolean} excludeContent whether the contents of files should be excluded
1665
+ * @returns {object} An object with the names and data of every file, or just the data of one file if a name was given
1666
+ */
1667
+ EpicEditor.prototype.getFiles = function (name, excludeContent) {
1668
+ var file
1669
+ , data = this._getFileStore(name);
1670
+
1671
+ if (name) {
1672
+ if (data !== undefined) {
1673
+ if (excludeContent) {
1674
+ delete data.content;
1675
+ }
1676
+ else {
1677
+ data.content = _sanitizeRawContent(data.content);
1678
+ }
1679
+ }
1680
+ return data;
1681
+ }
1682
+ else {
1683
+ for (file in data) {
1684
+ if (data.hasOwnProperty(file)) {
1685
+ if (excludeContent) {
1686
+ delete data[file].content;
1687
+ }
1688
+ else {
1689
+ data[file].content = _sanitizeRawContent(data[file].content);
1690
+ }
1691
+ }
1692
+ }
1693
+ return data;
1694
+ }
1695
+ }
1696
+
1697
+ // EVENTS
1698
+ // TODO: Support for namespacing events like "preview.foo"
1699
+ /**
1700
+ * Sets up an event handler for a specified event
1701
+ * @param {string} ev The event name
1702
+ * @param {function} handler The callback to run when the event fires
1703
+ * @returns {object} EpicEditor will be returned
1704
+ */
1705
+ EpicEditor.prototype.on = function (ev, handler) {
1706
+ var self = this;
1707
+ if (!this.events[ev]) {
1708
+ this.events[ev] = [];
1709
+ }
1710
+ this.events[ev].push(handler);
1711
+ return self;
1712
+ };
1713
+
1714
+ /**
1715
+ * This will emit or "trigger" an event specified
1716
+ * @param {string} ev The event name
1717
+ * @param {any} data Any data you want to pass into the callback
1718
+ * @returns {object} EpicEditor will be returned
1719
+ */
1720
+ EpicEditor.prototype.emit = function (ev, data) {
1721
+ var self = this
1722
+ , x;
1723
+
1724
+ data = data || self.getFiles(self.settings.file.name);
1725
+
1726
+ if (!this.events[ev]) {
1727
+ return;
1728
+ }
1729
+
1730
+ function invokeHandler(handler) {
1731
+ handler.call(self, data);
1732
+ }
1733
+
1734
+ for (x = 0; x < self.events[ev].length; x++) {
1735
+ invokeHandler(self.events[ev][x]);
1736
+ }
1737
+
1738
+ return self;
1739
+ };
1740
+
1741
+ /**
1742
+ * Will remove any listeners added from EpicEditor.on()
1743
+ * @param {string} ev The event name
1744
+ * @param {function} handler Handler to remove
1745
+ * @returns {object} EpicEditor will be returned
1746
+ */
1747
+ EpicEditor.prototype.removeListener = function (ev, handler) {
1748
+ var self = this;
1749
+ if (!handler) {
1750
+ this.events[ev] = [];
1751
+ return self;
1752
+ }
1753
+ if (!this.events[ev]) {
1754
+ return self;
1755
+ }
1756
+ // Otherwise a handler and event exist, so take care of it
1757
+ this.events[ev].splice(this.events[ev].indexOf(handler), 1);
1758
+ return self;
1759
+ }
1760
+
1761
+ /**
1762
+ * Handles autogrowing the editor
1763
+ */
1764
+ EpicEditor.prototype._autogrow = function () {
1765
+ var editorHeight
1766
+ , newHeight
1767
+ , minHeight
1768
+ , maxHeight
1769
+ , el
1770
+ , style
1771
+ , maxedOut = false;
1772
+
1773
+ //autogrow in fullscreen is nonsensical
1774
+ if (!this.is('fullscreen')) {
1775
+ if (this.is('edit')) {
1776
+ el = this.getElement('editor').documentElement;
1777
+ }
1778
+ else {
1779
+ el = this.getElement('previewer').documentElement;
1780
+ }
1781
+
1782
+ editorHeight = _outerHeight(el);
1783
+ newHeight = editorHeight;
1784
+
1785
+ //handle minimum
1786
+ minHeight = this.settings.autogrow.minHeight;
1787
+ if (typeof minHeight === 'function') {
1788
+ minHeight = minHeight(this);
1789
+ }
1790
+
1791
+ if (minHeight && newHeight < minHeight) {
1792
+ newHeight = minHeight;
1793
+ }
1794
+
1795
+ //handle maximum
1796
+ maxHeight = this.settings.autogrow.maxHeight;
1797
+ if (typeof maxHeight === 'function') {
1798
+ maxHeight = maxHeight(this);
1799
+ }
1800
+
1801
+ if (maxHeight && newHeight > maxHeight) {
1802
+ newHeight = maxHeight;
1803
+ maxedOut = true;
1804
+ }
1805
+
1806
+ if (maxedOut) {
1807
+ this._fixScrollbars('auto');
1808
+ } else {
1809
+ this._fixScrollbars('hidden');
1810
+ }
1811
+
1812
+ //actual resize
1813
+ if (newHeight != this.oldHeight) {
1814
+ this.getElement('container').style.height = newHeight + 'px';
1815
+ this.reflow();
1816
+ if (this.settings.autogrow.scroll) {
1817
+ window.scrollBy(0, newHeight - this.oldHeight);
1818
+ }
1819
+ this.oldHeight = newHeight;
1820
+ }
1821
+ }
1822
+ }
1823
+
1824
+ /**
1825
+ * Shows or hides scrollbars based on the autogrow setting
1826
+ * @param {string} forceSetting a value to force the overflow to
1827
+ */
1828
+ EpicEditor.prototype._fixScrollbars = function (forceSetting) {
1829
+ var setting;
1830
+ if (this.settings.autogrow) {
1831
+ setting = 'hidden';
1832
+ }
1833
+ else {
1834
+ setting = 'auto';
1835
+ }
1836
+ setting = forceSetting || setting;
1837
+ this.getElement('editor').documentElement.style.overflow = setting;
1838
+ this.getElement('previewer').documentElement.style.overflow = setting;
1839
+ }
1840
+
1841
+ EpicEditor.version = '0.2.2';
1842
+
1843
+ // Used to store information to be shared across editors
1844
+ EpicEditor._data = {};
1845
+
1846
+ window.EpicEditor = EpicEditor;
1847
+ })(window);
1848
+
1849
+ /**
1850
+ * marked - a markdown parser
1851
+ * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
1852
+ * https://github.com/chjj/marked
1853
+ */
1854
+
1855
+ ;(function() {
1856
+
1857
+ /**
1858
+ * Block-Level Grammar
1859
+ */
1860
+
1861
+ var block = {
1862
+ newline: /^\n+/,
1863
+ code: /^( {4}[^\n]+\n*)+/,
1864
+ fences: noop,
1865
+ hr: /^( *[-*_]){3,} *(?:\n+|$)/,
1866
+ heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
1867
+ nptable: noop,
1868
+ lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
1869
+ blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
1870
+ list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
1871
+ html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
1872
+ def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
1873
+ table: noop,
1874
+ paragraph: /^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/,
1875
+ text: /^[^\n]+/
1876
+ };
1877
+
1878
+ block.bullet = /(?:[*+-]|\d+\.)/;
1879
+ block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
1880
+ block.item = replace(block.item, 'gm')
1881
+ (/bull/g, block.bullet)
1882
+ ();
1883
+
1884
+ block.list = replace(block.list)
1885
+ (/bull/g, block.bullet)
1886
+ ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
1887
+ ();
1888
+
1889
+ block._tag = '(?!(?:'
1890
+ + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
1891
+ + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
1892
+ + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b';
1893
+
1894
+ block.html = replace(block.html)
1895
+ ('comment', /<!--[\s\S]*?-->/)
1896
+ ('closed', /<(tag)[\s\S]+?<\/\1>/)
1897
+ ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
1898
+ (/tag/g, block._tag)
1899
+ ();
1900
+
1901
+ block.paragraph = replace(block.paragraph)
1902
+ ('hr', block.hr)
1903
+ ('heading', block.heading)
1904
+ ('lheading', block.lheading)
1905
+ ('blockquote', block.blockquote)
1906
+ ('tag', '<' + block._tag)
1907
+ ('def', block.def)
1908
+ ();
1909
+
1910
+ /**
1911
+ * Normal Block Grammar
1912
+ */
1913
+
1914
+ block.normal = merge({}, block);
1915
+
1916
+ /**
1917
+ * GFM Block Grammar
1918
+ */
1919
+
1920
+ block.gfm = merge({}, block.normal, {
1921
+ fences: /^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
1922
+ paragraph: /^/
1923
+ });
1924
+
1925
+ block.gfm.paragraph = replace(block.paragraph)
1926
+ ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|')
1927
+ ();
1928
+
1929
+ /**
1930
+ * GFM + Tables Block Grammar
1931
+ */
1932
+
1933
+ block.tables = merge({}, block.gfm, {
1934
+ nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
1935
+ table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
1936
+ });
1937
+
1938
+ /**
1939
+ * Block Lexer
1940
+ */
1941
+
1942
+ function Lexer(options) {
1943
+ this.tokens = [];
1944
+ this.tokens.links = {};
1945
+ this.options = options || marked.defaults;
1946
+ this.rules = block.normal;
1947
+
1948
+ if (this.options.gfm) {
1949
+ if (this.options.tables) {
1950
+ this.rules = block.tables;
1951
+ } else {
1952
+ this.rules = block.gfm;
1953
+ }
1954
+ }
1955
+ }
1956
+
1957
+ /**
1958
+ * Expose Block Rules
1959
+ */
1960
+
1961
+ Lexer.rules = block;
1962
+
1963
+ /**
1964
+ * Static Lex Method
1965
+ */
1966
+
1967
+ Lexer.lex = function(src, options) {
1968
+ var lexer = new Lexer(options);
1969
+ return lexer.lex(src);
1970
+ };
1971
+
1972
+ /**
1973
+ * Preprocessing
1974
+ */
1975
+
1976
+ Lexer.prototype.lex = function(src) {
1977
+ src = src
1978
+ .replace(/\r\n|\r/g, '\n')
1979
+ .replace(/\t/g, ' ')
1980
+ .replace(/\u00a0/g, ' ')
1981
+ .replace(/\u2424/g, '\n');
1982
+
1983
+ return this.token(src, true);
1984
+ };
1985
+
1986
+ /**
1987
+ * Lexing
1988
+ */
1989
+
1990
+ Lexer.prototype.token = function(src, top) {
1991
+ var src = src.replace(/^ +$/gm, '')
1992
+ , next
1993
+ , loose
1994
+ , cap
1995
+ , item
1996
+ , space
1997
+ , i
1998
+ , l;
1999
+
2000
+ while (src) {
2001
+ // newline
2002
+ if (cap = this.rules.newline.exec(src)) {
2003
+ src = src.substring(cap[0].length);
2004
+ if (cap[0].length > 1) {
2005
+ this.tokens.push({
2006
+ type: 'space'
2007
+ });
2008
+ }
2009
+ }
2010
+
2011
+ // code
2012
+ if (cap = this.rules.code.exec(src)) {
2013
+ src = src.substring(cap[0].length);
2014
+ cap = cap[0].replace(/^ {4}/gm, '');
2015
+ this.tokens.push({
2016
+ type: 'code',
2017
+ text: !this.options.pedantic
2018
+ ? cap.replace(/\n+$/, '')
2019
+ : cap
2020
+ });
2021
+ continue;
2022
+ }
2023
+
2024
+ // fences (gfm)
2025
+ if (cap = this.rules.fences.exec(src)) {
2026
+ src = src.substring(cap[0].length);
2027
+ this.tokens.push({
2028
+ type: 'code',
2029
+ lang: cap[2],
2030
+ text: cap[3]
2031
+ });
2032
+ continue;
2033
+ }
2034
+
2035
+ // heading
2036
+ if (cap = this.rules.heading.exec(src)) {
2037
+ src = src.substring(cap[0].length);
2038
+ this.tokens.push({
2039
+ type: 'heading',
2040
+ depth: cap[1].length,
2041
+ text: cap[2]
2042
+ });
2043
+ continue;
2044
+ }
2045
+
2046
+ // table no leading pipe (gfm)
2047
+ if (top && (cap = this.rules.nptable.exec(src))) {
2048
+ src = src.substring(cap[0].length);
2049
+
2050
+ item = {
2051
+ type: 'table',
2052
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
2053
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
2054
+ cells: cap[3].replace(/\n$/, '').split('\n')
2055
+ };
2056
+
2057
+ for (i = 0; i < item.align.length; i++) {
2058
+ if (/^ *-+: *$/.test(item.align[i])) {
2059
+ item.align[i] = 'right';
2060
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
2061
+ item.align[i] = 'center';
2062
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
2063
+ item.align[i] = 'left';
2064
+ } else {
2065
+ item.align[i] = null;
2066
+ }
2067
+ }
2068
+
2069
+ for (i = 0; i < item.cells.length; i++) {
2070
+ item.cells[i] = item.cells[i].split(/ *\| */);
2071
+ }
2072
+
2073
+ this.tokens.push(item);
2074
+
2075
+ continue;
2076
+ }
2077
+
2078
+ // lheading
2079
+ if (cap = this.rules.lheading.exec(src)) {
2080
+ src = src.substring(cap[0].length);
2081
+ this.tokens.push({
2082
+ type: 'heading',
2083
+ depth: cap[2] === '=' ? 1 : 2,
2084
+ text: cap[1]
2085
+ });
2086
+ continue;
2087
+ }
2088
+
2089
+ // hr
2090
+ if (cap = this.rules.hr.exec(src)) {
2091
+ src = src.substring(cap[0].length);
2092
+ this.tokens.push({
2093
+ type: 'hr'
2094
+ });
2095
+ continue;
2096
+ }
2097
+
2098
+ // blockquote
2099
+ if (cap = this.rules.blockquote.exec(src)) {
2100
+ src = src.substring(cap[0].length);
2101
+
2102
+ this.tokens.push({
2103
+ type: 'blockquote_start'
2104
+ });
2105
+
2106
+ cap = cap[0].replace(/^ *> ?/gm, '');
2107
+
2108
+ // Pass `top` to keep the current
2109
+ // "toplevel" state. This is exactly
2110
+ // how markdown.pl works.
2111
+ this.token(cap, top);
2112
+
2113
+ this.tokens.push({
2114
+ type: 'blockquote_end'
2115
+ });
2116
+
2117
+ continue;
2118
+ }
2119
+
2120
+ // list
2121
+ if (cap = this.rules.list.exec(src)) {
2122
+ src = src.substring(cap[0].length);
2123
+
2124
+ this.tokens.push({
2125
+ type: 'list_start',
2126
+ ordered: isFinite(cap[2])
2127
+ });
2128
+
2129
+ // Get each top-level item.
2130
+ cap = cap[0].match(this.rules.item);
2131
+
2132
+ next = false;
2133
+ l = cap.length;
2134
+ i = 0;
2135
+
2136
+ for (; i < l; i++) {
2137
+ item = cap[i];
2138
+
2139
+ // Remove the list item's bullet
2140
+ // so it is seen as the next token.
2141
+ space = item.length;
2142
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
2143
+
2144
+ // Outdent whatever the
2145
+ // list item contains. Hacky.
2146
+ if (~item.indexOf('\n ')) {
2147
+ space -= item.length;
2148
+ item = !this.options.pedantic
2149
+ ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
2150
+ : item.replace(/^ {1,4}/gm, '');
2151
+ }
2152
+
2153
+ // Determine whether item is loose or not.
2154
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
2155
+ // for discount behavior.
2156
+ loose = next || /\n\n(?!\s*$)/.test(item);
2157
+ if (i !== l - 1) {
2158
+ next = item[item.length-1] === '\n';
2159
+ if (!loose) loose = next;
2160
+ }
2161
+
2162
+ this.tokens.push({
2163
+ type: loose
2164
+ ? 'loose_item_start'
2165
+ : 'list_item_start'
2166
+ });
2167
+
2168
+ // Recurse.
2169
+ this.token(item, false);
2170
+
2171
+ this.tokens.push({
2172
+ type: 'list_item_end'
2173
+ });
2174
+ }
2175
+
2176
+ this.tokens.push({
2177
+ type: 'list_end'
2178
+ });
2179
+
2180
+ continue;
2181
+ }
2182
+
2183
+ // html
2184
+ if (cap = this.rules.html.exec(src)) {
2185
+ src = src.substring(cap[0].length);
2186
+ this.tokens.push({
2187
+ type: this.options.sanitize
2188
+ ? 'paragraph'
2189
+ : 'html',
2190
+ pre: cap[1] === 'pre',
2191
+ text: cap[0]
2192
+ });
2193
+ continue;
2194
+ }
2195
+
2196
+ // def
2197
+ if (top && (cap = this.rules.def.exec(src))) {
2198
+ src = src.substring(cap[0].length);
2199
+ this.tokens.links[cap[1].toLowerCase()] = {
2200
+ href: cap[2],
2201
+ title: cap[3]
2202
+ };
2203
+ continue;
2204
+ }
2205
+
2206
+ // table (gfm)
2207
+ if (top && (cap = this.rules.table.exec(src))) {
2208
+ src = src.substring(cap[0].length);
2209
+
2210
+ item = {
2211
+ type: 'table',
2212
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
2213
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
2214
+ cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
2215
+ };
2216
+
2217
+ for (i = 0; i < item.align.length; i++) {
2218
+ if (/^ *-+: *$/.test(item.align[i])) {
2219
+ item.align[i] = 'right';
2220
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
2221
+ item.align[i] = 'center';
2222
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
2223
+ item.align[i] = 'left';
2224
+ } else {
2225
+ item.align[i] = null;
2226
+ }
2227
+ }
2228
+
2229
+ for (i = 0; i < item.cells.length; i++) {
2230
+ item.cells[i] = item.cells[i]
2231
+ .replace(/^ *\| *| *\| *$/g, '')
2232
+ .split(/ *\| */);
2233
+ }
2234
+
2235
+ this.tokens.push(item);
2236
+
2237
+ continue;
2238
+ }
2239
+
2240
+ // top-level paragraph
2241
+ if (top && (cap = this.rules.paragraph.exec(src))) {
2242
+ src = src.substring(cap[0].length);
2243
+ this.tokens.push({
2244
+ type: 'paragraph',
2245
+ text: cap[0]
2246
+ });
2247
+ continue;
2248
+ }
2249
+
2250
+ // text
2251
+ if (cap = this.rules.text.exec(src)) {
2252
+ // Top-level should never reach here.
2253
+ src = src.substring(cap[0].length);
2254
+ this.tokens.push({
2255
+ type: 'text',
2256
+ text: cap[0]
2257
+ });
2258
+ continue;
2259
+ }
2260
+
2261
+ if (src) {
2262
+ throw new
2263
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
2264
+ }
2265
+ }
2266
+
2267
+ return this.tokens;
2268
+ };
2269
+
2270
+ /**
2271
+ * Inline-Level Grammar
2272
+ */
2273
+
2274
+ var inline = {
2275
+ escape: /^\\([\\`*{}\[\]()#+\-.!_>|])/,
2276
+ autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
2277
+ url: noop,
2278
+ tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
2279
+ link: /^!?\[(inside)\]\(href\)/,
2280
+ reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
2281
+ nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
2282
+ strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
2283
+ em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
2284
+ code: /^(`+)([\s\S]*?[^`])\1(?!`)/,
2285
+ br: /^ {2,}\n(?!\s*$)/,
2286
+ del: noop,
2287
+ text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
2288
+ };
2289
+
2290
+ inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
2291
+ inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
2292
+
2293
+ inline.link = replace(inline.link)
2294
+ ('inside', inline._inside)
2295
+ ('href', inline._href)
2296
+ ();
2297
+
2298
+ inline.reflink = replace(inline.reflink)
2299
+ ('inside', inline._inside)
2300
+ ();
2301
+
2302
+ /**
2303
+ * Normal Inline Grammar
2304
+ */
2305
+
2306
+ inline.normal = merge({}, inline);
2307
+
2308
+ /**
2309
+ * Pedantic Inline Grammar
2310
+ */
2311
+
2312
+ inline.pedantic = merge({}, inline.normal, {
2313
+ strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
2314
+ em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
2315
+ });
2316
+
2317
+ /**
2318
+ * GFM Inline Grammar
2319
+ */
2320
+
2321
+ inline.gfm = merge({}, inline.normal, {
2322
+ escape: replace(inline.escape)('])', '~])')(),
2323
+ url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,
2324
+ del: /^~{2,}([\s\S]+?)~{2,}/,
2325
+ text: replace(inline.text)
2326
+ (']|', '~]|')
2327
+ ('|', '|https?://|')
2328
+ ()
2329
+ });
2330
+
2331
+ /**
2332
+ * GFM + Line Breaks Inline Grammar
2333
+ */
2334
+
2335
+ inline.breaks = merge({}, inline.gfm, {
2336
+ br: replace(inline.br)('{2,}', '*')(),
2337
+ text: replace(inline.gfm.text)('{2,}', '*')()
2338
+ });
2339
+
2340
+ /**
2341
+ * Inline Lexer & Compiler
2342
+ */
2343
+
2344
+ function InlineLexer(links, options) {
2345
+ this.options = options || marked.defaults;
2346
+ this.links = links;
2347
+ this.rules = inline.normal;
2348
+
2349
+ if (!this.links) {
2350
+ throw new
2351
+ Error('Tokens array requires a `links` property.');
2352
+ }
2353
+
2354
+ if (this.options.gfm) {
2355
+ if (this.options.breaks) {
2356
+ this.rules = inline.breaks;
2357
+ } else {
2358
+ this.rules = inline.gfm;
2359
+ }
2360
+ } else if (this.options.pedantic) {
2361
+ this.rules = inline.pedantic;
2362
+ }
2363
+ }
2364
+
2365
+ /**
2366
+ * Expose Inline Rules
2367
+ */
2368
+
2369
+ InlineLexer.rules = inline;
2370
+
2371
+ /**
2372
+ * Static Lexing/Compiling Method
2373
+ */
2374
+
2375
+ InlineLexer.output = function(src, links, opt) {
2376
+ var inline = new InlineLexer(links, opt);
2377
+ return inline.output(src);
2378
+ };
2379
+
2380
+ /**
2381
+ * Lexing/Compiling
2382
+ */
2383
+
2384
+ InlineLexer.prototype.output = function(src) {
2385
+ var out = ''
2386
+ , link
2387
+ , text
2388
+ , href
2389
+ , cap;
2390
+
2391
+ while (src) {
2392
+ // escape
2393
+ if (cap = this.rules.escape.exec(src)) {
2394
+ src = src.substring(cap[0].length);
2395
+ out += cap[1];
2396
+ continue;
2397
+ }
2398
+
2399
+ // autolink
2400
+ if (cap = this.rules.autolink.exec(src)) {
2401
+ src = src.substring(cap[0].length);
2402
+ if (cap[2] === '@') {
2403
+ text = cap[1][6] === ':'
2404
+ ? this.mangle(cap[1].substring(7))
2405
+ : this.mangle(cap[1]);
2406
+ href = this.mangle('mailto:') + text;
2407
+ } else {
2408
+ text = escape(cap[1]);
2409
+ href = text;
2410
+ }
2411
+ out += '<a href="'
2412
+ + href
2413
+ + '">'
2414
+ + text
2415
+ + '</a>';
2416
+ continue;
2417
+ }
2418
+
2419
+ // url (gfm)
2420
+ if (cap = this.rules.url.exec(src)) {
2421
+ src = src.substring(cap[0].length);
2422
+ text = escape(cap[1]);
2423
+ href = text;
2424
+ out += '<a href="'
2425
+ + href
2426
+ + '">'
2427
+ + text
2428
+ + '</a>';
2429
+ continue;
2430
+ }
2431
+
2432
+ // tag
2433
+ if (cap = this.rules.tag.exec(src)) {
2434
+ src = src.substring(cap[0].length);
2435
+ out += this.options.sanitize
2436
+ ? escape(cap[0])
2437
+ : cap[0];
2438
+ continue;
2439
+ }
2440
+
2441
+ // link
2442
+ if (cap = this.rules.link.exec(src)) {
2443
+ src = src.substring(cap[0].length);
2444
+ out += this.outputLink(cap, {
2445
+ href: cap[2],
2446
+ title: cap[3]
2447
+ });
2448
+ continue;
2449
+ }
2450
+
2451
+ // reflink, nolink
2452
+ if ((cap = this.rules.reflink.exec(src))
2453
+ || (cap = this.rules.nolink.exec(src))) {
2454
+ src = src.substring(cap[0].length);
2455
+ link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
2456
+ link = this.links[link.toLowerCase()];
2457
+ if (!link || !link.href) {
2458
+ out += cap[0][0];
2459
+ src = cap[0].substring(1) + src;
2460
+ continue;
2461
+ }
2462
+ out += this.outputLink(cap, link);
2463
+ continue;
2464
+ }
2465
+
2466
+ // strong
2467
+ if (cap = this.rules.strong.exec(src)) {
2468
+ src = src.substring(cap[0].length);
2469
+ out += '<strong>'
2470
+ + this.output(cap[2] || cap[1])
2471
+ + '</strong>';
2472
+ continue;
2473
+ }
2474
+
2475
+ // em
2476
+ if (cap = this.rules.em.exec(src)) {
2477
+ src = src.substring(cap[0].length);
2478
+ out += '<em>'
2479
+ + this.output(cap[2] || cap[1])
2480
+ + '</em>';
2481
+ continue;
2482
+ }
2483
+
2484
+ // code
2485
+ if (cap = this.rules.code.exec(src)) {
2486
+ src = src.substring(cap[0].length);
2487
+ out += '<code>'
2488
+ + escape(cap[2], true)
2489
+ + '</code>';
2490
+ continue;
2491
+ }
2492
+
2493
+ // br
2494
+ if (cap = this.rules.br.exec(src)) {
2495
+ src = src.substring(cap[0].length);
2496
+ out += '<br>';
2497
+ continue;
2498
+ }
2499
+
2500
+ // del (gfm)
2501
+ if (cap = this.rules.del.exec(src)) {
2502
+ src = src.substring(cap[0].length);
2503
+ out += '<del>'
2504
+ + this.output(cap[1])
2505
+ + '</del>';
2506
+ continue;
2507
+ }
2508
+
2509
+ // text
2510
+ if (cap = this.rules.text.exec(src)) {
2511
+ src = src.substring(cap[0].length);
2512
+ out += escape(cap[0]);
2513
+ continue;
2514
+ }
2515
+
2516
+ if (src) {
2517
+ throw new
2518
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
2519
+ }
2520
+ }
2521
+
2522
+ return out;
2523
+ };
2524
+
2525
+ /**
2526
+ * Compile Link
2527
+ */
2528
+
2529
+ InlineLexer.prototype.outputLink = function(cap, link) {
2530
+ if (cap[0][0] !== '!') {
2531
+ return '<a href="'
2532
+ + escape(link.href)
2533
+ + '"'
2534
+ + (link.title
2535
+ ? ' title="'
2536
+ + escape(link.title)
2537
+ + '"'
2538
+ : '')
2539
+ + '>'
2540
+ + this.output(cap[1])
2541
+ + '</a>';
2542
+ } else {
2543
+ return '<img src="'
2544
+ + escape(link.href)
2545
+ + '" alt="'
2546
+ + escape(cap[1])
2547
+ + '"'
2548
+ + (link.title
2549
+ ? ' title="'
2550
+ + escape(link.title)
2551
+ + '"'
2552
+ : '')
2553
+ + '>';
2554
+ }
2555
+ };
2556
+
2557
+ /**
2558
+ * Mangle Links
2559
+ */
2560
+
2561
+ InlineLexer.prototype.mangle = function(text) {
2562
+ var out = ''
2563
+ , l = text.length
2564
+ , i = 0
2565
+ , ch;
2566
+
2567
+ for (; i < l; i++) {
2568
+ ch = text.charCodeAt(i);
2569
+ if (Math.random() > 0.5) {
2570
+ ch = 'x' + ch.toString(16);
2571
+ }
2572
+ out += '&#' + ch + ';';
2573
+ }
2574
+
2575
+ return out;
2576
+ };
2577
+
2578
+ /**
2579
+ * Parsing & Compiling
2580
+ */
2581
+
2582
+ function Parser(options) {
2583
+ this.tokens = [];
2584
+ this.token = null;
2585
+ this.options = options || marked.defaults;
2586
+ }
2587
+
2588
+ /**
2589
+ * Static Parse Method
2590
+ */
2591
+
2592
+ Parser.parse = function(src, options) {
2593
+ var parser = new Parser(options);
2594
+ return parser.parse(src);
2595
+ };
2596
+
2597
+ /**
2598
+ * Parse Loop
2599
+ */
2600
+
2601
+ Parser.prototype.parse = function(src) {
2602
+ this.inline = new InlineLexer(src.links, this.options);
2603
+ this.tokens = src.reverse();
2604
+
2605
+ var out = '';
2606
+ while (this.next()) {
2607
+ out += this.tok();
2608
+ }
2609
+
2610
+ return out;
2611
+ };
2612
+
2613
+ /**
2614
+ * Next Token
2615
+ */
2616
+
2617
+ Parser.prototype.next = function() {
2618
+ return this.token = this.tokens.pop();
2619
+ };
2620
+
2621
+ /**
2622
+ * Preview Next Token
2623
+ */
2624
+
2625
+ Parser.prototype.peek = function() {
2626
+ return this.tokens[this.tokens.length-1] || 0;
2627
+ };
2628
+
2629
+ /**
2630
+ * Parse Text Tokens
2631
+ */
2632
+
2633
+ Parser.prototype.parseText = function() {
2634
+ var body = this.token.text;
2635
+
2636
+ while (this.peek().type === 'text') {
2637
+ body += '\n' + this.next().text;
2638
+ }
2639
+
2640
+ return this.inline.output(body);
2641
+ };
2642
+
2643
+ /**
2644
+ * Parse Current Token
2645
+ */
2646
+
2647
+ Parser.prototype.tok = function() {
2648
+ switch (this.token.type) {
2649
+ case 'space': {
2650
+ return '';
2651
+ }
2652
+ case 'hr': {
2653
+ return '<hr>\n';
2654
+ }
2655
+ case 'heading': {
2656
+ return '<h'
2657
+ + this.token.depth
2658
+ + '>'
2659
+ + this.inline.output(this.token.text)
2660
+ + '</h'
2661
+ + this.token.depth
2662
+ + '>\n';
2663
+ }
2664
+ case 'code': {
2665
+ if (this.options.highlight) {
2666
+ var code = this.options.highlight(this.token.text, this.token.lang);
2667
+ if (code != null && code !== this.token.text) {
2668
+ this.token.escaped = true;
2669
+ this.token.text = code;
2670
+ }
2671
+ }
2672
+
2673
+ if (!this.token.escaped) {
2674
+ this.token.text = escape(this.token.text, true);
2675
+ }
2676
+
2677
+ return '<pre><code'
2678
+ + (this.token.lang
2679
+ ? ' class="lang-'
2680
+ + this.token.lang
2681
+ + '"'
2682
+ : '')
2683
+ + '>'
2684
+ + this.token.text
2685
+ + '</code></pre>\n';
2686
+ }
2687
+ case 'table': {
2688
+ var body = ''
2689
+ , heading
2690
+ , i
2691
+ , row
2692
+ , cell
2693
+ , j;
2694
+
2695
+ // header
2696
+ body += '<thead>\n<tr>\n';
2697
+ for (i = 0; i < this.token.header.length; i++) {
2698
+ heading = this.inline.output(this.token.header[i]);
2699
+ body += this.token.align[i]
2700
+ ? '<th align="' + this.token.align[i] + '">' + heading + '</th>\n'
2701
+ : '<th>' + heading + '</th>\n';
2702
+ }
2703
+ body += '</tr>\n</thead>\n';
2704
+
2705
+ // body
2706
+ body += '<tbody>\n'
2707
+ for (i = 0; i < this.token.cells.length; i++) {
2708
+ row = this.token.cells[i];
2709
+ body += '<tr>\n';
2710
+ for (j = 0; j < row.length; j++) {
2711
+ cell = this.inline.output(row[j]);
2712
+ body += this.token.align[j]
2713
+ ? '<td align="' + this.token.align[j] + '">' + cell + '</td>\n'
2714
+ : '<td>' + cell + '</td>\n';
2715
+ }
2716
+ body += '</tr>\n';
2717
+ }
2718
+ body += '</tbody>\n';
2719
+
2720
+ return '<table>\n'
2721
+ + body
2722
+ + '</table>\n';
2723
+ }
2724
+ case 'blockquote_start': {
2725
+ var body = '';
2726
+
2727
+ while (this.next().type !== 'blockquote_end') {
2728
+ body += this.tok();
2729
+ }
2730
+
2731
+ return '<blockquote>\n'
2732
+ + body
2733
+ + '</blockquote>\n';
2734
+ }
2735
+ case 'list_start': {
2736
+ var type = this.token.ordered ? 'ol' : 'ul'
2737
+ , body = '';
2738
+
2739
+ while (this.next().type !== 'list_end') {
2740
+ body += this.tok();
2741
+ }
2742
+
2743
+ return '<'
2744
+ + type
2745
+ + '>\n'
2746
+ + body
2747
+ + '</'
2748
+ + type
2749
+ + '>\n';
2750
+ }
2751
+ case 'list_item_start': {
2752
+ var body = '';
2753
+
2754
+ while (this.next().type !== 'list_item_end') {
2755
+ body += this.token.type === 'text'
2756
+ ? this.parseText()
2757
+ : this.tok();
2758
+ }
2759
+
2760
+ return '<li>'
2761
+ + body
2762
+ + '</li>\n';
2763
+ }
2764
+ case 'loose_item_start': {
2765
+ var body = '';
2766
+
2767
+ while (this.next().type !== 'list_item_end') {
2768
+ body += this.tok();
2769
+ }
2770
+
2771
+ return '<li>'
2772
+ + body
2773
+ + '</li>\n';
2774
+ }
2775
+ case 'html': {
2776
+ return !this.token.pre && !this.options.pedantic
2777
+ ? this.inline.output(this.token.text)
2778
+ : this.token.text;
2779
+ }
2780
+ case 'paragraph': {
2781
+ return '<p>'
2782
+ + this.inline.output(this.token.text)
2783
+ + '</p>\n';
2784
+ }
2785
+ case 'text': {
2786
+ return '<p>'
2787
+ + this.parseText()
2788
+ + '</p>\n';
2789
+ }
2790
+ }
2791
+ };
2792
+
2793
+ /**
2794
+ * Helpers
2795
+ */
2796
+
2797
+ function escape(html, encode) {
2798
+ return html
2799
+ .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
2800
+ .replace(/</g, '&lt;')
2801
+ .replace(/>/g, '&gt;')
2802
+ .replace(/"/g, '&quot;')
2803
+ .replace(/'/g, '&#39;');
2804
+ }
2805
+
2806
+ function replace(regex, opt) {
2807
+ regex = regex.source;
2808
+ opt = opt || '';
2809
+ return function self(name, val) {
2810
+ if (!name) return new RegExp(regex, opt);
2811
+ val = val.source || val;
2812
+ val = val.replace(/(^|[^\[])\^/g, '$1');
2813
+ regex = regex.replace(name, val);
2814
+ return self;
2815
+ };
2816
+ }
2817
+
2818
+ function noop() {}
2819
+ noop.exec = noop;
2820
+
2821
+ function merge(obj) {
2822
+ var i = 1
2823
+ , target
2824
+ , key;
2825
+
2826
+ for (; i < arguments.length; i++) {
2827
+ target = arguments[i];
2828
+ for (key in target) {
2829
+ if (Object.prototype.hasOwnProperty.call(target, key)) {
2830
+ obj[key] = target[key];
2831
+ }
2832
+ }
2833
+ }
2834
+
2835
+ return obj;
2836
+ }
2837
+
2838
+ /**
2839
+ * Marked
2840
+ */
2841
+
2842
+ function marked(src, opt) {
2843
+ try {
2844
+ return Parser.parse(Lexer.lex(src, opt), opt);
2845
+ } catch (e) {
2846
+ e.message += '\nPlease report this to https://github.com/chjj/marked.';
2847
+ if ((opt || marked.defaults).silent) {
2848
+ return 'An error occured:\n' + e.message;
2849
+ }
2850
+ throw e;
2851
+ }
2852
+ }
2853
+
2854
+ /**
2855
+ * Options
2856
+ */
2857
+
2858
+ marked.options =
2859
+ marked.setOptions = function(opt) {
2860
+ marked.defaults = opt;
2861
+ return marked;
2862
+ };
2863
+
2864
+ marked.defaults = {
2865
+ gfm: true,
2866
+ tables: true,
2867
+ breaks: false,
2868
+ pedantic: false,
2869
+ sanitize: false,
2870
+ silent: false,
2871
+ highlight: null
2872
+ };
2873
+
2874
+ /**
2875
+ * Expose
2876
+ */
2877
+
2878
+ marked.Parser = Parser;
2879
+ marked.parser = Parser.parse;
2880
+
2881
+ marked.Lexer = Lexer;
2882
+ marked.lexer = Lexer.lex;
2883
+
2884
+ marked.InlineLexer = InlineLexer;
2885
+ marked.inlineLexer = InlineLexer.output;
2886
+
2887
+ marked.parse = marked;
2888
+
2889
+ if (typeof module !== 'undefined') {
2890
+ module.exports = marked;
2891
+ } else if (typeof define === 'function' && define.amd) {
2892
+ define(function() { return marked; });
2893
+ } else {
2894
+ this.marked = marked;
2895
+ }
2896
+
2897
+ }).call(function() {
2898
+ return this || (typeof window !== 'undefined' ? window : global);
2899
+ }());