epiceditor 0.0.0 → 0.2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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
+ }());