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