epic-editor-rails 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2e2d6a131bb8837b5fb67dd22da072c61ea28a72
4
- data.tar.gz: bed9bd3f3daf68d6a98d0fbd10194a3bc2f1ff1f
3
+ metadata.gz: a1f61c947de147d8aa143a1547e78afbbda43d34
4
+ data.tar.gz: d1b706acb83e9d1ef6d6b80581aab2baf7e719db
5
5
  SHA512:
6
- metadata.gz: c2dbad36e67f382cdfbcbad33dddc01cd3189151293f9c6a3018c6b2144fca6a926c5b1d42d66a88e7c384b4e9fad45f4a203dd6488204047933aca0527c83a6
7
- data.tar.gz: 66e8f2a694fc8bd273d014b40beba94d5951fa9b80aff0e822123ecb2fb5cbd5cece416b06d4cec9575234afe2ef290ef83fd6fc64a8c5ced67d2571e9c6b56f
6
+ metadata.gz: 880a40b82f1ca2b17837a92ababfcfa22d3b8d6c3ab91102bda439ad0d77ce01a04f56851673a84f92848c3b7eadc06bc0c31da535f2d1ddaaea31fb84c8ba6a
7
+ data.tar.gz: c51b928266245d803e523d96a9e565d61d4b84f4b4999c4e233fbc5952e4bbfa9323d90642a4c6760a5e110c717e8868e6ee8229769ec007cd06f7b989269634
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- epic-editor-rails v0.1.1
1
+ epic-editor-rails v0.2.2
2
2
  =================
3
3
  Gemfile: ```gem 'epic-editor-rails'```
4
4
  Install: ```$ bundle install ```
@@ -18,5 +18,5 @@ application.css.(scss):
18
18
  @import 'editor/epic-light';
19
19
  ```
20
20
 
21
- EpicEditor v0.2.0
21
+ EpicEditor v0.2.2
22
22
  http://epiceditor.com/
@@ -1,7 +1,7 @@
1
1
  module Epic
2
2
  module Editor
3
3
  module Rails
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
6
6
  end
7
7
  end
@@ -51,7 +51,7 @@
51
51
  }
52
52
 
53
53
  /**
54
- * Saves the current style state for the styles requested, then applys styles
54
+ * Saves the current style state for the styles requested, then applies styles
55
55
  * to overwrite the existing one. The old styles are returned as an object so
56
56
  * you can pass it back in when you want to revert back to the old style
57
57
  * @param {object} el The element to get the styles of
@@ -101,7 +101,7 @@
101
101
  function _outerHeight(el) {
102
102
  var b = parseInt(_getStyle(el, 'border-top-width'), 10) + parseInt(_getStyle(el, 'border-bottom-width'), 10)
103
103
  , p = parseInt(_getStyle(el, 'padding-top'), 10) + parseInt(_getStyle(el, 'padding-bottom'), 10)
104
- , w = el.offsetHeight
104
+ , w = parseInt(_getStyle(el, 'height'), 10)
105
105
  , t;
106
106
  // For IE in case no border is set and it defaults to "medium"
107
107
  if (isNaN(b)) { b = 0; }
@@ -165,27 +165,38 @@
165
165
  }
166
166
 
167
167
  function _setText(el, content) {
168
- // If you want to know why we check for typeof string, see comment
169
- // in the _getText function
170
- if (typeof document.body.innerText == 'string') {
171
- content = content.replace(/ /g, '\u00a0');
172
- el.innerText = content;
173
- }
174
- else {
175
- // Don't convert lt/gt characters as HTML when viewing the editor window
176
- // TODO: Write a test to catch regressions for this
177
- content = content.replace(/</g, '&lt;');
178
- content = content.replace(/>/g, '&gt;');
179
- content = content.replace(/\n/g, '<br>');
180
- // Make sure to look for TWO spaces and replace with a space and &nbsp;
181
- // If you find and replace every space with a &nbsp; text will not wrap.
182
- // Hence the name (Non-Breaking-SPace).
183
- content = content.replace(/\s\s/g, ' &nbsp;')
184
- el.innerHTML = content;
185
- }
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;
186
184
  return true;
187
185
  }
188
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
+
189
200
  /**
190
201
  * Will return the version number if the browser is IE. If not will return -1
191
202
  * TRY NEVER TO USE THIS AND USE FEATURE DETECTION IF POSSIBLE
@@ -216,6 +227,16 @@
216
227
  return n.userAgent.indexOf('Safari') > -1 && n.userAgent.indexOf('Chrome') == -1;
217
228
  }
218
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
+
219
240
  /**
220
241
  * Determines if supplied value is a function
221
242
  * @param {object} object to determine type
@@ -305,7 +326,8 @@
305
326
  , _defaultFileSchema
306
327
  , _defaultFile
307
328
  , defaults = { container: 'epiceditor'
308
- , basePath: 'epiceditor'
329
+ , basePath: ''
330
+ , textarea: undefined
309
331
  , clientSideStorage: true
310
332
  , localStorageName: 'epiceditor'
311
333
  , useNativeFullscreen: true
@@ -322,11 +344,29 @@
322
344
  , fullscreen: 70 // f keycode
323
345
  , preview: 80 // p keycode
324
346
  }
347
+ , string: { togglePreview: 'Toggle Preview Mode'
348
+ , toggleEdit: 'Toggle Edit Mode'
349
+ , toggleFullscreen: 'Enter Fullscreen'
350
+ }
325
351
  , parser: typeof marked == 'function' ? marked : null
352
+ , autogrow: false
353
+ , button: { fullscreen: true
354
+ , preview: true
355
+ , bar: "auto"
356
+ }
326
357
  }
327
- , defaultStorage;
358
+ , defaultStorage
359
+ , autogrowDefaults = { minHeight: 80
360
+ , maxHeight: false
361
+ , scroll: true
362
+ };
328
363
 
329
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;
330
370
 
331
371
  if (!(typeof self.settings.parser == 'function' && typeof self.settings.parser('TEST') == 'string')) {
332
372
  self.settings.parser = function (str) {
@@ -334,6 +374,29 @@
334
374
  }
335
375
  }
336
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
+ }
337
400
 
338
401
  // Grab the container element and save it to self.element
339
402
  // if it's a string assume it's an ID and if it's an object
@@ -366,6 +429,14 @@
366
429
  }
367
430
  }
368
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
+
369
440
  // Protect the id and overwrite if passed in as an option
370
441
  // TODO: Put underscrore to denote that this is private
371
442
  self._instanceId = 'epiceditor-' + Math.round(Math.random() * 100000);
@@ -384,7 +455,6 @@
384
455
  if (localStorage && self.settings.clientSideStorage) {
385
456
  this._storage = localStorage;
386
457
  if (this._storage[self.settings.localStorageName] && self.getFiles(self.settings.file.name) === undefined) {
387
- _defaultFile = self.getFiles(self.settings.file.name);
388
458
  _defaultFile = self._defaultFileSchema();
389
459
  _defaultFile.content = self.settings.file.defaultContent;
390
460
  }
@@ -397,6 +467,11 @@
397
467
  this._storage[self.settings.localStorageName] = defaultStorage;
398
468
  }
399
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
+
400
475
  // This needs to replace the use of classes to check the state of EE
401
476
  self._eeState = {
402
477
  fullscreen: false
@@ -437,20 +512,32 @@
437
512
  , _elementStates
438
513
  , _isInEdit
439
514
  , nativeFs = false
515
+ , nativeFsWebkit = false
516
+ , nativeFsMoz = false
517
+ , nativeFsW3C = false
440
518
  , fsElement
441
519
  , isMod = false
442
520
  , isCtrl = false
443
521
  , eventableIframes
444
- , i; // i is reused for loops
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;
445
528
 
446
529
  if (self.settings.useNativeFullscreen) {
447
- nativeFs = document.body.webkitRequestFullScreen ? true : false
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;
448
534
  }
449
535
 
450
536
  // Fucking Safari's native fullscreen works terribly
451
537
  // REMOVE THIS IF SAFARI 7 WORKS BETTER
452
538
  if (_isSafari()) {
453
539
  nativeFs = false;
540
+ nativeFsWebkit = false;
454
541
  }
455
542
 
456
543
  // It opens edit mode by default (for now);
@@ -468,14 +555,15 @@
468
555
  '<iframe frameborder="0" id="epiceditor-editor-frame"></iframe>' +
469
556
  '<iframe frameborder="0" id="epiceditor-previewer-frame"></iframe>' +
470
557
  '<div id="epiceditor-utilbar">' +
471
- '<img width="30" src="<%= asset_path("preview.png") %>" title="Toggle Preview Mode" class="epiceditor-toggle-btn epiceditor-toggle-preview-btn"> ' +
472
- '<img width="30" src="<%= asset_path("edit.png") %>" title="Toggle Edit Mode" class="epiceditor-toggle-btn epiceditor-toggle-edit-btn"> ' +
473
- '<img width="30" src="<%= asset_path("fullscreen.png") %>" title="Enter Fullscreen" class="epiceditor-fullscreen-btn">' +
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>' : '') +
474
561
  '</div>' +
475
562
  '</div>'
476
563
 
477
564
  // The previewer is just an empty box for the generated HTML to go into
478
565
  , previewer: '<div id="epiceditor-preview"></div>'
566
+ , editor: '<!doctype HTML>'
479
567
  };
480
568
 
481
569
  // Write an iframe and then select it for the editor
@@ -506,7 +594,7 @@
506
594
  self.editorIframeDocument = _getIframeInnards(self.editorIframe);
507
595
  self.editorIframeDocument.open();
508
596
  // Need something for... you guessed it, Firefox
509
- self.editorIframeDocument.write('');
597
+ self.editorIframeDocument.write(_HtmlTemplates.editor);
510
598
  self.editorIframeDocument.close();
511
599
 
512
600
  // Setup the previewer iframe
@@ -535,6 +623,10 @@
535
623
  // Add a relative style to the overall wrapper to keep CSS relative to the editor
536
624
  self.iframe.getElementById('epiceditor-wrapper').style.position = 'relative';
537
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
+
538
630
  // Now grab the editor and previewer for later use
539
631
  self.editor = self.editorIframeDocument.body;
540
632
  self.previewer = self.previewerIframeDocument.getElementById('epiceditor-preview');
@@ -545,7 +637,10 @@
545
637
  self.iframe.body.style.height = this.element.offsetHeight + 'px';
546
638
 
547
639
  // Should actually check what mode it's in!
548
- this.previewerIframe.style.display = 'none';
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';
549
644
 
550
645
  // FIXME figure out why it needs +2 px
551
646
  if (_isIE() > -1) {
@@ -560,23 +655,53 @@
560
655
  // iframe's ready state == complete, then we can focus on the contenteditable
561
656
  self.iframe.addEventListener('readystatechange', function () {
562
657
  if (self.iframe.readyState == 'complete') {
563
- self.editorIframeDocument.body.focus();
658
+ self.focus();
564
659
  }
565
660
  });
566
661
  }
567
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
+
568
683
  utilBtns = self.iframe.getElementById('epiceditor-utilbar');
569
684
 
685
+ // TODO: Move into fullscreen setup function (_setupFullscreen)
570
686
  _elementStates = {}
571
687
  self._goFullscreen = function (el) {
572
-
688
+ this._fixScrollbars('auto');
689
+
573
690
  if (self.is('fullscreen')) {
574
691
  self._exitFullscreen(el);
575
692
  return;
576
693
  }
577
694
 
578
695
  if (nativeFs) {
579
- el.webkitRequestFullScreen();
696
+ if (nativeFsWebkit) {
697
+ el.webkitRequestFullScreen();
698
+ }
699
+ else if (nativeFsMoz) {
700
+ el.mozRequestFullScreen();
701
+ }
702
+ else if (nativeFsW3C) {
703
+ el.requestFullscreen();
704
+ }
580
705
  }
581
706
 
582
707
  _isInEdit = self.is('edit');
@@ -608,6 +733,8 @@
608
733
  , 'cssFloat': 'left' // FF
609
734
  , 'styleFloat': 'left' // Older IEs
610
735
  , 'display': 'block'
736
+ , 'position': 'static'
737
+ , 'left': ''
611
738
  });
612
739
 
613
740
  // the previewer
@@ -618,6 +745,8 @@
618
745
  , 'cssFloat': 'right' // FF
619
746
  , 'styleFloat': 'right' // Older IEs
620
747
  , 'display': 'block'
748
+ , 'position': 'static'
749
+ , 'left': ''
621
750
  });
622
751
 
623
752
  // Setup the containing element CSS for fullscreen
@@ -650,12 +779,14 @@
650
779
 
651
780
  self.preview();
652
781
 
653
- self.editorIframeDocument.body.focus();
782
+ self.focus();
654
783
 
655
784
  self.emit('fullscreenenter');
656
785
  };
657
786
 
658
787
  self._exitFullscreen = function (el) {
788
+ this._fixScrollbars();
789
+
659
790
  _saveStyleState(self.element, 'apply', _elementStates.element);
660
791
  _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement);
661
792
  _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe);
@@ -669,15 +800,26 @@
669
800
 
670
801
  utilBtns.style.visibility = 'visible';
671
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
+
672
809
  if (!nativeFs) {
673
810
  document.body.style.overflow = 'auto';
674
811
  }
675
812
  else {
676
- document.webkitCancelFullScreen();
813
+ if (nativeFsWebkit) {
814
+ document.webkitCancelFullScreen();
815
+ }
816
+ else if (nativeFsMoz) {
817
+ document.mozCancelFullScreen();
818
+ }
819
+ else if (nativeFsW3C) {
820
+ document.exitFullscreen();
821
+ }
677
822
  }
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
823
 
682
824
  if (_isInEdit) {
683
825
  self.edit();
@@ -720,18 +862,35 @@
720
862
  });
721
863
 
722
864
  // Sets up the NATIVE fullscreen editor/previewer for WebKit
723
- if (document.body.webkitRequestFullScreen) {
724
- fsElement.addEventListener('webkitfullscreenchange', function () {
725
- if (!document.webkitIsFullScreen) {
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) {
726
882
  self._exitFullscreen(fsElement);
727
883
  }
728
884
  }, false);
729
885
  }
730
886
 
887
+ // TODO: Move utilBar stuff into a utilBar setup function (_setupUtilBar)
731
888
  utilBar = self.iframe.getElementById('epiceditor-utilbar');
732
889
 
733
890
  // Hide it at first until they move their mouse
734
- utilBar.style.display = 'none';
891
+ if (self.settings.button.bar !== true) {
892
+ utilBar.style.display = 'none';
893
+ }
735
894
 
736
895
  utilBar.addEventListener('mouseover', function () {
737
896
  if (utilBarTimer) {
@@ -740,6 +899,9 @@
740
899
  });
741
900
 
742
901
  function utilBarHandler(e) {
902
+ if (self.settings.button.bar !== "auto") {
903
+ return;
904
+ }
743
905
  // Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code
744
906
  // we do this for 2 reasons:
745
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
@@ -768,15 +930,15 @@
768
930
  // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview
769
931
  if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) {
770
932
  e.preventDefault();
771
- if (self.is('edit')) {
933
+ if (self.is('edit') && self._previewEnabled) {
772
934
  self.preview();
773
935
  }
774
- else {
936
+ else if (self._editEnabled) {
775
937
  self.edit();
776
938
  }
777
939
  }
778
940
  // Check for alt+f - default shortcut to make editor fullscreen
779
- if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen) {
941
+ if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen && self._fullscreenEnabled) {
780
942
  e.preventDefault();
781
943
  self._goFullscreen(fsElement);
782
944
  }
@@ -812,6 +974,29 @@
812
974
  if (e.keyCode == 17) { isCtrl = false }
813
975
  }
814
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
+
815
1000
  // Hide and show the util bar based on mouse movements
816
1001
  eventableIframes = [self.previewerIframeDocument, self.editorIframeDocument];
817
1002
 
@@ -828,22 +1013,32 @@
828
1013
  eventableIframes[i].addEventListener('keydown', function (e) {
829
1014
  shortcutHandler(e);
830
1015
  });
1016
+ eventableIframes[i].addEventListener('paste', function (e) {
1017
+ pasteHandler(e);
1018
+ });
831
1019
  }
832
1020
 
833
1021
  // Save the document every 100ms by default
1022
+ // TODO: Move into autosave setup function (_setupAutoSave)
834
1023
  if (self.settings.file.autoSave) {
835
- self.saveInterval = window.setInterval(function () {
1024
+ self._saveIntervalTimer = window.setInterval(function () {
836
1025
  if (!self._canSave) {
837
1026
  return;
838
1027
  }
839
- self.save();
1028
+ self.save(false, true);
840
1029
  }, self.settings.file.autoSave);
841
1030
  }
842
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
+
843
1038
  window.addEventListener('resize', function () {
844
1039
  // If NOT webkit, and in fullscreen, we need to account for browser resizing
845
1040
  // we don't care about webkit because you can't resize in webkit's fullscreen
846
- if (!self.iframe.webkitRequestFullScreen && self.is('fullscreen')) {
1041
+ if (self.is('fullscreen')) {
847
1042
  _applyStyles(self.iframeElement, {
848
1043
  'width': window.outerWidth + 'px'
849
1044
  , 'height': window.innerHeight + 'px'
@@ -881,12 +1076,118 @@
881
1076
  }
882
1077
 
883
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
+
884
1108
  // The callback and call are the same thing, but different ways to access them
885
1109
  callback.call(this);
886
1110
  this.emit('load');
887
1111
  return this;
888
1112
  }
889
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
+
890
1191
  /**
891
1192
  * Will remove the editor, but not offline files
892
1193
  * @returns {object} EpicEditor will be returned
@@ -905,11 +1206,19 @@
905
1206
  self._eeState.loaded = false;
906
1207
  self._eeState.unloaded = true;
907
1208
  callback = callback || function () {};
908
-
909
- if (self.saveInterval) {
910
- window.clearInterval(self.saveInterval);
1209
+
1210
+ if (self.settings.textarea) {
1211
+ self._textareaElement.value = "";
1212
+ self.removeListener('__update');
911
1213
  }
912
-
1214
+
1215
+ if (self._saveIntervalTimer) {
1216
+ window.clearInterval(self._saveIntervalTimer);
1217
+ }
1218
+ if (self._textareaSaveTimer) {
1219
+ window.clearInterval(self._textareaSaveTimer);
1220
+ }
1221
+
913
1222
  callback.call(this);
914
1223
  self.emit('unload');
915
1224
  return self;
@@ -919,47 +1228,65 @@
919
1228
  * reflow allows you to dynamically re-fit the editor in the parent without
920
1229
  * having to unload and then reload the editor again.
921
1230
  *
922
- * @param {string} kind Can either be 'width' or 'height' or null
923
- * if null, both the height and width will be resized
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' }
924
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.
925
1240
  * @returns {object} EpicEditor will be returned
926
1241
  */
927
- EpicEditor.prototype.reflow = function (kind) {
1242
+ EpicEditor.prototype.reflow = function (kind, callback) {
928
1243
  var self = this
929
1244
  , widthDiff = _outerWidth(self.element) - self.element.offsetWidth
930
1245
  , heightDiff = _outerHeight(self.element) - self.element.offsetHeight
931
1246
  , elements = [self.iframeElement, self.editorIframe, self.previewerIframe]
1247
+ , eventData = {}
932
1248
  , newWidth
933
1249
  , newHeight;
934
1250
 
1251
+ if (typeof kind == 'function') {
1252
+ callback = kind;
1253
+ kind = null;
1254
+ }
1255
+
1256
+ if (!callback) {
1257
+ callback = function () {};
1258
+ }
935
1259
 
936
1260
  for (var x = 0; x < elements.length; x++) {
937
1261
  if (!kind || kind == 'width') {
938
1262
  newWidth = self.element.offsetWidth - widthDiff + 'px';
939
1263
  elements[x].style.width = newWidth;
940
1264
  self._eeState.reflowWidth = newWidth;
1265
+ eventData.width = newWidth;
941
1266
  }
942
1267
  if (!kind || kind == 'height') {
943
1268
  newHeight = self.element.offsetHeight - heightDiff + 'px';
944
1269
  elements[x].style.height = newHeight;
945
1270
  self._eeState.reflowHeight = newHeight
1271
+ eventData.height = newHeight;
946
1272
  }
947
1273
  }
1274
+
1275
+ self.emit('reflow', eventData);
1276
+ callback.call(this, eventData);
948
1277
  return self;
949
1278
  }
950
1279
 
951
1280
  /**
952
1281
  * 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
1282
  * @returns {object} EpicEditor will be returned
955
1283
  */
956
- EpicEditor.prototype.preview = function (theme) {
1284
+ EpicEditor.prototype.preview = function () {
957
1285
  var self = this
958
1286
  , x
1287
+ , theme = self.settings.theme.preview
959
1288
  , anchors;
960
1289
 
961
- theme = theme || self.settings.basePath + self.settings.theme.preview;
962
-
963
1290
  _replaceClass(self.getElement('wrapper'), 'epiceditor-edit-mode', 'epiceditor-preview-mode');
964
1291
 
965
1292
  // Check if no CSS theme link exists
@@ -970,33 +1297,45 @@
970
1297
  self.previewerIframeDocument.getElementById('theme').href = theme;
971
1298
  }
972
1299
 
973
- // Add the generated HTML into the previewer
974
- self.previewer.innerHTML = self.exportFile(null, 'html');
1300
+ // Save a preview draft since it might not be saved to the real file yet
1301
+ self.save(true);
975
1302
 
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
- }
1303
+ // Add the generated draft HTML into the previewer
1304
+ self.previewer.innerHTML = self.exportFile(null, 'html', true);
986
1305
 
987
1306
  // Hide the editor and display the previewer
988
1307
  if (!self.is('fullscreen')) {
989
- self.editorIframe.style.display = 'none';
990
- self.previewerIframe.style.display = 'block';
1308
+ self.editorIframe.style.left = '-999999px';
1309
+ self.previewerIframe.style.left = '';
991
1310
  self._eeState.preview = true;
992
1311
  self._eeState.edit = false;
993
- self.previewerIframe.focus();
1312
+ self._focusExceptOnLoad();
994
1313
  }
995
-
1314
+
996
1315
  self.emit('preview');
997
1316
  return self;
998
1317
  }
999
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
+
1000
1339
  /**
1001
1340
  * Puts the editor into fullscreen mode
1002
1341
  * @returns {object} EpicEditor will be returned
@@ -1026,9 +1365,9 @@
1026
1365
  _replaceClass(self.getElement('wrapper'), 'epiceditor-preview-mode', 'epiceditor-edit-mode');
1027
1366
  self._eeState.preview = false;
1028
1367
  self._eeState.edit = true;
1029
- self.editorIframe.style.display = 'block';
1030
- self.previewerIframe.style.display = 'none';
1031
- self.editorIframe.focus();
1368
+ self.editorIframe.style.left = '';
1369
+ self.previewerIframe.style.left = '-999999px';
1370
+ self._focusExceptOnLoad();
1032
1371
  self.emit('edit');
1033
1372
  return this;
1034
1373
  }
@@ -1077,6 +1416,10 @@
1077
1416
  return self._eeState.edit;
1078
1417
  case 'fullscreen':
1079
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;
1080
1423
  default:
1081
1424
  return false;
1082
1425
  }
@@ -1094,9 +1437,9 @@
1094
1437
  name = name || self.settings.file.name;
1095
1438
  self.settings.file.name = name;
1096
1439
  if (this._storage[self.settings.localStorageName]) {
1097
- fileObj = self.getFiles();
1098
- if (fileObj[name] !== undefined) {
1099
- _setText(self.editor, fileObj[name].content);
1440
+ fileObj = self.exportFile(name);
1441
+ if (fileObj !== undefined) {
1442
+ _setText(self.editor, fileObj);
1100
1443
  self.emit('read');
1101
1444
  }
1102
1445
  else {
@@ -1114,40 +1457,62 @@
1114
1457
  * Saves content for offline use
1115
1458
  * @returns {object} EpicEditor will be returned
1116
1459
  */
1117
- EpicEditor.prototype.save = function () {
1460
+ EpicEditor.prototype.save = function (_isPreviewDraft, _isAuto) {
1118
1461
  var self = this
1119
1462
  , storage
1120
1463
  , isUpdate = false
1121
1464
  , file = self.settings.file.name
1465
+ , previewDraftName = ''
1466
+ , data = this._storage[previewDraftName + self.settings.localStorageName]
1122
1467
  , content = _getText(this.editor);
1123
1468
 
1469
+ if (_isPreviewDraft) {
1470
+ previewDraftName = self._previewDraftLocation;
1471
+ }
1472
+
1124
1473
  // This could have been false but since we're manually saving
1125
1474
  // we know it's save to start autoSaving again
1126
1475
  this._canSave = true;
1127
1476
 
1128
- storage = JSON.parse(this._storage[self.settings.localStorageName]);
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]);
1129
1481
 
1130
- // If the file doesn't exist we need to create it
1131
- if (storage[file] === undefined) {
1132
- storage[file] = self._defaultFileSchema();
1133
- }
1482
+ // If the file doesn't exist we need to create it
1483
+ if (storage[file] === undefined) {
1484
+ storage[file] = self._defaultFileSchema();
1485
+ }
1134
1486
 
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
- }
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
+ }
1141
1497
 
1142
- storage[file].content = content;
1143
- this._storage[self.settings.localStorageName] = JSON.stringify(storage);
1498
+ storage[file].content = content;
1499
+ this._storage[previewDraftName + self.settings.localStorageName] = JSON.stringify(storage);
1144
1500
 
1145
- // After the content is actually changed, emit update so it emits the updated content
1146
- if (isUpdate) {
1147
- self.emit('update');
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
+ }
1148
1514
  }
1149
1515
 
1150
- this.emit('save');
1151
1516
  return this;
1152
1517
  }
1153
1518
 
@@ -1224,16 +1589,43 @@
1224
1589
  self.preview();
1225
1590
  }
1226
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
+
1227
1599
  return this;
1228
1600
  };
1229
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
+
1230
1622
  /**
1231
1623
  * Exports a file as a string in a supported format
1232
1624
  * @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)
1625
+ * @param {string} kind Kind of file you want the content in (currently supports html and text, default is the format the browser "wants")
1234
1626
  * @returns {string|undefined} The content of the file in the content given or undefined if it doesn't exist
1235
1627
  */
1236
- EpicEditor.prototype.exportFile = function (name, kind) {
1628
+ EpicEditor.prototype.exportFile = function (name, kind, _isPreviewDraft) {
1237
1629
  var self = this
1238
1630
  , file
1239
1631
  , content;
@@ -1241,7 +1633,7 @@
1241
1633
  name = name || self.settings.file.name;
1242
1634
  kind = kind || 'text';
1243
1635
 
1244
- file = self.getFiles(name);
1636
+ file = self._getFileStore(name, _isPreviewDraft);
1245
1637
 
1246
1638
  // If the file doesn't exist just return early with undefined
1247
1639
  if (file === undefined) {
@@ -1252,27 +1644,53 @@
1252
1644
 
1253
1645
  switch (kind) {
1254
1646
  case 'html':
1255
- // Get this, 2 spaces in a content editable actually converts to:
1256
- // 0020 00a0, meaning, "space no-break space". So, manually convert
1257
- // no-break spaces to spaces again before handing to marked.
1258
- // Also, WebKit converts no-break to unicode equivalent and FF HTML.
1259
- content = content.replace(/\u00a0/g, ' ').replace(/&nbsp;/g, ' ');
1647
+ content = _sanitizeRawContent(content);
1260
1648
  return self.settings.parser(content);
1261
1649
  case 'text':
1262
- content = content.replace(/\u00a0/g, ' ').replace(/&nbsp;/g, ' ');
1650
+ return _sanitizeRawContent(content);
1651
+ case 'json':
1652
+ file.content = _sanitizeRawContent(file.content);
1653
+ return JSON.stringify(file);
1654
+ case 'raw':
1263
1655
  return content;
1264
1656
  default:
1265
1657
  return content;
1266
1658
  }
1267
1659
  }
1268
1660
 
1269
- EpicEditor.prototype.getFiles = function (name) {
1270
- var files = JSON.parse(this._storage[this.settings.localStorageName]);
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
+
1271
1671
  if (name) {
1272
- return files[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;
1273
1681
  }
1274
1682
  else {
1275
- return files;
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;
1276
1694
  }
1277
1695
  }
1278
1696
 
@@ -1340,7 +1758,87 @@
1340
1758
  return self;
1341
1759
  }
1342
1760
 
1343
- EpicEditor.version = '0.2.0';
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';
1344
1842
 
1345
1843
  // Used to store information to be shared across editors
1346
1844
  EpicEditor._data = {};
@@ -1349,8 +1847,9 @@
1349
1847
  })(window);
1350
1848
 
1351
1849
  /**
1352
- * marked - A markdown parser (https://github.com/chjj/marked)
1353
- * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed)
1850
+ * marked - a markdown parser
1851
+ * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
1852
+ * https://github.com/chjj/marked
1354
1853
  */
1355
1854
 
1356
1855
  ;(function() {
@@ -1365,12 +1864,14 @@ var block = {
1365
1864
  fences: noop,
1366
1865
  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
1367
1866
  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
1867
+ nptable: noop,
1368
1868
  lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
1369
1869
  blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
1370
- list: /^( *)(bull) [^\0]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
1870
+ list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
1371
1871
  html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
1372
1872
  def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
1373
- paragraph: /^([^\n]+\n?(?!body))+\n*/,
1873
+ table: noop,
1874
+ paragraph: /^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/,
1374
1875
  text: /^[^\n]+/
1375
1876
  };
1376
1877
 
@@ -1385,64 +1886,108 @@ block.list = replace(block.list)
1385
1886
  ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
1386
1887
  ();
1387
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
+
1388
1894
  block.html = replace(block.html)
1389
- ('comment', /<!--[^\0]*?-->/)
1390
- ('closed', /<(tag)[^\0]+?<\/\1>/)
1391
- ('closing', /<tag(?!:\/|@)\b(?:"[^"]*"|'[^']*'|[^'">])*?>/)
1392
- (/tag/g, tag())
1895
+ ('comment', /<!--[\s\S]*?-->/)
1896
+ ('closed', /<(tag)[\s\S]+?<\/\1>/)
1897
+ ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
1898
+ (/tag/g, block._tag)
1393
1899
  ();
1394
1900
 
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
- };
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
+ */
1419
1913
 
1420
- block.gfm = {
1421
- fences: /^ *``` *(\w+)? *\n([^\0]+?)\s*``` *(?:\n+|$)/,
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+|$)/,
1422
1922
  paragraph: /^/
1423
- };
1923
+ });
1424
1924
 
1425
1925
  block.gfm.paragraph = replace(block.paragraph)
1426
- ('(?!', '(?!' + block.gfm.fences.source.replace(/(^|[^\[])\^/g, '$1') + '|')
1926
+ ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|')
1427
1927
  ();
1428
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
+
1429
1938
  /**
1430
1939
  * Block Lexer
1431
1940
  */
1432
1941
 
1433
- block.lexer = function(src) {
1434
- var tokens = [];
1942
+ function Lexer(options) {
1943
+ this.tokens = [];
1944
+ this.tokens.links = {};
1945
+ this.options = options || marked.defaults;
1946
+ this.rules = block.normal;
1435
1947
 
1436
- tokens.links = {};
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;
1437
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) {
1438
1977
  src = src
1439
1978
  .replace(/\r\n|\r/g, '\n')
1440
- .replace(/\t/g, ' ');
1979
+ .replace(/\t/g, ' ')
1980
+ .replace(/\u00a0/g, ' ')
1981
+ .replace(/\u2424/g, '\n');
1441
1982
 
1442
- return block.token(src, tokens, true);
1983
+ return this.token(src, true);
1443
1984
  };
1444
1985
 
1445
- block.token = function(src, tokens, top) {
1986
+ /**
1987
+ * Lexing
1988
+ */
1989
+
1990
+ Lexer.prototype.token = function(src, top) {
1446
1991
  var src = src.replace(/^ +$/gm, '')
1447
1992
  , next
1448
1993
  , loose
@@ -1454,22 +1999,22 @@ block.token = function(src, tokens, top) {
1454
1999
 
1455
2000
  while (src) {
1456
2001
  // newline
1457
- if (cap = block.newline.exec(src)) {
2002
+ if (cap = this.rules.newline.exec(src)) {
1458
2003
  src = src.substring(cap[0].length);
1459
2004
  if (cap[0].length > 1) {
1460
- tokens.push({
2005
+ this.tokens.push({
1461
2006
  type: 'space'
1462
2007
  });
1463
2008
  }
1464
2009
  }
1465
2010
 
1466
2011
  // code
1467
- if (cap = block.code.exec(src)) {
2012
+ if (cap = this.rules.code.exec(src)) {
1468
2013
  src = src.substring(cap[0].length);
1469
2014
  cap = cap[0].replace(/^ {4}/gm, '');
1470
- tokens.push({
2015
+ this.tokens.push({
1471
2016
  type: 'code',
1472
- text: !options.pedantic
2017
+ text: !this.options.pedantic
1473
2018
  ? cap.replace(/\n+$/, '')
1474
2019
  : cap
1475
2020
  });
@@ -1477,20 +2022,20 @@ block.token = function(src, tokens, top) {
1477
2022
  }
1478
2023
 
1479
2024
  // fences (gfm)
1480
- if (cap = block.fences.exec(src)) {
2025
+ if (cap = this.rules.fences.exec(src)) {
1481
2026
  src = src.substring(cap[0].length);
1482
- tokens.push({
2027
+ this.tokens.push({
1483
2028
  type: 'code',
1484
- lang: cap[1],
1485
- text: cap[2]
2029
+ lang: cap[2],
2030
+ text: cap[3]
1486
2031
  });
1487
2032
  continue;
1488
2033
  }
1489
2034
 
1490
2035
  // heading
1491
- if (cap = block.heading.exec(src)) {
2036
+ if (cap = this.rules.heading.exec(src)) {
1492
2037
  src = src.substring(cap[0].length);
1493
- tokens.push({
2038
+ this.tokens.push({
1494
2039
  type: 'heading',
1495
2040
  depth: cap[1].length,
1496
2041
  text: cap[2]
@@ -1498,10 +2043,42 @@ block.token = function(src, tokens, top) {
1498
2043
  continue;
1499
2044
  }
1500
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
+
1501
2078
  // lheading
1502
- if (cap = block.lheading.exec(src)) {
2079
+ if (cap = this.rules.lheading.exec(src)) {
1503
2080
  src = src.substring(cap[0].length);
1504
- tokens.push({
2081
+ this.tokens.push({
1505
2082
  type: 'heading',
1506
2083
  depth: cap[2] === '=' ? 1 : 2,
1507
2084
  text: cap[1]
@@ -1510,19 +2087,19 @@ block.token = function(src, tokens, top) {
1510
2087
  }
1511
2088
 
1512
2089
  // hr
1513
- if (cap = block.hr.exec(src)) {
2090
+ if (cap = this.rules.hr.exec(src)) {
1514
2091
  src = src.substring(cap[0].length);
1515
- tokens.push({
2092
+ this.tokens.push({
1516
2093
  type: 'hr'
1517
2094
  });
1518
2095
  continue;
1519
2096
  }
1520
2097
 
1521
2098
  // blockquote
1522
- if (cap = block.blockquote.exec(src)) {
2099
+ if (cap = this.rules.blockquote.exec(src)) {
1523
2100
  src = src.substring(cap[0].length);
1524
2101
 
1525
- tokens.push({
2102
+ this.tokens.push({
1526
2103
  type: 'blockquote_start'
1527
2104
  });
1528
2105
 
@@ -1531,9 +2108,9 @@ block.token = function(src, tokens, top) {
1531
2108
  // Pass `top` to keep the current
1532
2109
  // "toplevel" state. This is exactly
1533
2110
  // how markdown.pl works.
1534
- block.token(cap, tokens, top);
2111
+ this.token(cap, top);
1535
2112
 
1536
- tokens.push({
2113
+ this.tokens.push({
1537
2114
  type: 'blockquote_end'
1538
2115
  });
1539
2116
 
@@ -1541,16 +2118,16 @@ block.token = function(src, tokens, top) {
1541
2118
  }
1542
2119
 
1543
2120
  // list
1544
- if (cap = block.list.exec(src)) {
2121
+ if (cap = this.rules.list.exec(src)) {
1545
2122
  src = src.substring(cap[0].length);
1546
2123
 
1547
- tokens.push({
2124
+ this.tokens.push({
1548
2125
  type: 'list_start',
1549
2126
  ordered: isFinite(cap[2])
1550
2127
  });
1551
2128
 
1552
2129
  // Get each top-level item.
1553
- cap = cap[0].match(block.item);
2130
+ cap = cap[0].match(this.rules.item);
1554
2131
 
1555
2132
  next = false;
1556
2133
  l = cap.length;
@@ -1568,7 +2145,7 @@ block.token = function(src, tokens, top) {
1568
2145
  // list item contains. Hacky.
1569
2146
  if (~item.indexOf('\n ')) {
1570
2147
  space -= item.length;
1571
- item = !options.pedantic
2148
+ item = !this.options.pedantic
1572
2149
  ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
1573
2150
  : item.replace(/^ {1,4}/gm, '');
1574
2151
  }
@@ -1582,21 +2159,21 @@ block.token = function(src, tokens, top) {
1582
2159
  if (!loose) loose = next;
1583
2160
  }
1584
2161
 
1585
- tokens.push({
2162
+ this.tokens.push({
1586
2163
  type: loose
1587
2164
  ? 'loose_item_start'
1588
2165
  : 'list_item_start'
1589
2166
  });
1590
2167
 
1591
2168
  // Recurse.
1592
- block.token(item, tokens);
2169
+ this.token(item, false);
1593
2170
 
1594
- tokens.push({
2171
+ this.tokens.push({
1595
2172
  type: 'list_item_end'
1596
2173
  });
1597
2174
  }
1598
2175
 
1599
- tokens.push({
2176
+ this.tokens.push({
1600
2177
  type: 'list_end'
1601
2178
  });
1602
2179
 
@@ -1604,10 +2181,12 @@ block.token = function(src, tokens, top) {
1604
2181
  }
1605
2182
 
1606
2183
  // html
1607
- if (cap = block.html.exec(src)) {
2184
+ if (cap = this.rules.html.exec(src)) {
1608
2185
  src = src.substring(cap[0].length);
1609
- tokens.push({
1610
- type: 'html',
2186
+ this.tokens.push({
2187
+ type: this.options.sanitize
2188
+ ? 'paragraph'
2189
+ : 'html',
1611
2190
  pre: cap[1] === 'pre',
1612
2191
  text: cap[0]
1613
2192
  });
@@ -1615,19 +2194,53 @@ block.token = function(src, tokens, top) {
1615
2194
  }
1616
2195
 
1617
2196
  // def
1618
- if (top && (cap = block.def.exec(src))) {
2197
+ if (top && (cap = this.rules.def.exec(src))) {
1619
2198
  src = src.substring(cap[0].length);
1620
- tokens.links[cap[1].toLowerCase()] = {
2199
+ this.tokens.links[cap[1].toLowerCase()] = {
1621
2200
  href: cap[2],
1622
2201
  title: cap[3]
1623
2202
  };
1624
2203
  continue;
1625
2204
  }
1626
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
+
1627
2240
  // top-level paragraph
1628
- if (top && (cap = block.paragraph.exec(src))) {
2241
+ if (top && (cap = this.rules.paragraph.exec(src))) {
1629
2242
  src = src.substring(cap[0].length);
1630
- tokens.push({
2243
+ this.tokens.push({
1631
2244
  type: 'paragraph',
1632
2245
  text: cap[0]
1633
2246
  });
@@ -1635,75 +2248,141 @@ block.token = function(src, tokens, top) {
1635
2248
  }
1636
2249
 
1637
2250
  // text
1638
- if (cap = block.text.exec(src)) {
2251
+ if (cap = this.rules.text.exec(src)) {
1639
2252
  // Top-level should never reach here.
1640
2253
  src = src.substring(cap[0].length);
1641
- tokens.push({
2254
+ this.tokens.push({
1642
2255
  type: 'text',
1643
2256
  text: cap[0]
1644
2257
  });
1645
2258
  continue;
1646
2259
  }
2260
+
2261
+ if (src) {
2262
+ throw new
2263
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
2264
+ }
1647
2265
  }
1648
2266
 
1649
- return tokens;
2267
+ return this.tokens;
1650
2268
  };
1651
2269
 
1652
2270
  /**
1653
- * Inline Processing
2271
+ * Inline-Level Grammar
1654
2272
  */
1655
2273
 
1656
2274
  var inline = {
1657
- escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
2275
+ escape: /^\\([\\`*{}\[\]()#+\-.!_>|])/,
1658
2276
  autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
1659
2277
  url: noop,
1660
- tag: /^<!--[^\0]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
2278
+ tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
1661
2279
  link: /^!?\[(inside)\]\(href\)/,
1662
2280
  reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
1663
2281
  nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
1664
- strong: /^__([^\0]+?)__(?!_)|^\*\*([^\0]+?)\*\*(?!\*)/,
1665
- em: /^\b_((?:__|[^\0])+?)_\b|^\*((?:\*\*|[^\0])+?)\*(?!\*)/,
1666
- code: /^(`+)([^\0]*?[^`])\1(?!`)/,
2282
+ strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
2283
+ em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
2284
+ code: /^(`+)([\s\S]*?[^`])\1(?!`)/,
1667
2285
  br: /^ {2,}\n(?!\s*$)/,
1668
- text: /^[^\0]+?(?=[\\<!\[_*`]| {2,}\n|$)/
2286
+ del: noop,
2287
+ text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
1669
2288
  };
1670
2289
 
1671
- inline._linkInside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
1672
- inline._linkHref = /\s*<?([^\s]*?)>?(?:\s+['"]([^\0]*?)['"])?\s*/;
2290
+ inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
2291
+ inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
1673
2292
 
1674
2293
  inline.link = replace(inline.link)
1675
- ('inside', inline._linkInside)
1676
- ('href', inline._linkHref)
2294
+ ('inside', inline._inside)
2295
+ ('href', inline._href)
1677
2296
  ();
1678
2297
 
1679
2298
  inline.reflink = replace(inline.reflink)
1680
- ('inside', inline._linkInside)
2299
+ ('inside', inline._inside)
1681
2300
  ();
1682
2301
 
1683
- inline.normal = {
1684
- url: inline.url,
1685
- strong: inline.strong,
1686
- em: inline.em,
1687
- text: inline.text
1688
- };
2302
+ /**
2303
+ * Normal Inline Grammar
2304
+ */
1689
2305
 
1690
- inline.pedantic = {
1691
- strong: /^__(?=\S)([^\0]*?\S)__(?!_)|^\*\*(?=\S)([^\0]*?\S)\*\*(?!\*)/,
1692
- em: /^_(?=\S)([^\0]*?\S)_(?!_)|^\*(?=\S)([^\0]*?\S)\*(?!\*)/
1693
- };
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
+ });
1694
2316
 
1695
- inline.gfm = {
2317
+ /**
2318
+ * GFM Inline Grammar
2319
+ */
2320
+
2321
+ inline.gfm = merge({}, inline.normal, {
2322
+ escape: replace(inline.escape)('])', '~])')(),
1696
2323
  url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,
1697
- text: /^[^\0]+?(?=[\\<!\[_*`]|https?:\/\/| {2,}\n|$)/
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);
1698
2378
  };
1699
2379
 
1700
2380
  /**
1701
- * Inline Lexer
2381
+ * Lexing/Compiling
1702
2382
  */
1703
2383
 
1704
- inline.lexer = function(src) {
2384
+ InlineLexer.prototype.output = function(src) {
1705
2385
  var out = ''
1706
- , links = tokens.links
1707
2386
  , link
1708
2387
  , text
1709
2388
  , href
@@ -1711,20 +2390,20 @@ inline.lexer = function(src) {
1711
2390
 
1712
2391
  while (src) {
1713
2392
  // escape
1714
- if (cap = inline.escape.exec(src)) {
2393
+ if (cap = this.rules.escape.exec(src)) {
1715
2394
  src = src.substring(cap[0].length);
1716
2395
  out += cap[1];
1717
2396
  continue;
1718
2397
  }
1719
2398
 
1720
2399
  // autolink
1721
- if (cap = inline.autolink.exec(src)) {
2400
+ if (cap = this.rules.autolink.exec(src)) {
1722
2401
  src = src.substring(cap[0].length);
1723
2402
  if (cap[2] === '@') {
1724
2403
  text = cap[1][6] === ':'
1725
- ? mangle(cap[1].substring(7))
1726
- : mangle(cap[1]);
1727
- href = mangle('mailto:') + text;
2404
+ ? this.mangle(cap[1].substring(7))
2405
+ : this.mangle(cap[1]);
2406
+ href = this.mangle('mailto:') + text;
1728
2407
  } else {
1729
2408
  text = escape(cap[1]);
1730
2409
  href = text;
@@ -1738,7 +2417,7 @@ inline.lexer = function(src) {
1738
2417
  }
1739
2418
 
1740
2419
  // url (gfm)
1741
- if (cap = inline.url.exec(src)) {
2420
+ if (cap = this.rules.url.exec(src)) {
1742
2421
  src = src.substring(cap[0].length);
1743
2422
  text = escape(cap[1]);
1744
2423
  href = text;
@@ -1751,18 +2430,18 @@ inline.lexer = function(src) {
1751
2430
  }
1752
2431
 
1753
2432
  // tag
1754
- if (cap = inline.tag.exec(src)) {
2433
+ if (cap = this.rules.tag.exec(src)) {
1755
2434
  src = src.substring(cap[0].length);
1756
- out += options.sanitize
2435
+ out += this.options.sanitize
1757
2436
  ? escape(cap[0])
1758
2437
  : cap[0];
1759
2438
  continue;
1760
2439
  }
1761
2440
 
1762
2441
  // link
1763
- if (cap = inline.link.exec(src)) {
2442
+ if (cap = this.rules.link.exec(src)) {
1764
2443
  src = src.substring(cap[0].length);
1765
- out += outputLink(cap, {
2444
+ out += this.outputLink(cap, {
1766
2445
  href: cap[2],
1767
2446
  title: cap[3]
1768
2447
  });
@@ -1770,40 +2449,40 @@ inline.lexer = function(src) {
1770
2449
  }
1771
2450
 
1772
2451
  // reflink, nolink
1773
- if ((cap = inline.reflink.exec(src))
1774
- || (cap = inline.nolink.exec(src))) {
2452
+ if ((cap = this.rules.reflink.exec(src))
2453
+ || (cap = this.rules.nolink.exec(src))) {
1775
2454
  src = src.substring(cap[0].length);
1776
2455
  link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
1777
- link = links[link.toLowerCase()];
2456
+ link = this.links[link.toLowerCase()];
1778
2457
  if (!link || !link.href) {
1779
2458
  out += cap[0][0];
1780
2459
  src = cap[0].substring(1) + src;
1781
2460
  continue;
1782
2461
  }
1783
- out += outputLink(cap, link);
2462
+ out += this.outputLink(cap, link);
1784
2463
  continue;
1785
2464
  }
1786
2465
 
1787
2466
  // strong
1788
- if (cap = inline.strong.exec(src)) {
2467
+ if (cap = this.rules.strong.exec(src)) {
1789
2468
  src = src.substring(cap[0].length);
1790
2469
  out += '<strong>'
1791
- + inline.lexer(cap[2] || cap[1])
2470
+ + this.output(cap[2] || cap[1])
1792
2471
  + '</strong>';
1793
2472
  continue;
1794
2473
  }
1795
2474
 
1796
2475
  // em
1797
- if (cap = inline.em.exec(src)) {
2476
+ if (cap = this.rules.em.exec(src)) {
1798
2477
  src = src.substring(cap[0].length);
1799
2478
  out += '<em>'
1800
- + inline.lexer(cap[2] || cap[1])
2479
+ + this.output(cap[2] || cap[1])
1801
2480
  + '</em>';
1802
2481
  continue;
1803
2482
  }
1804
2483
 
1805
2484
  // code
1806
- if (cap = inline.code.exec(src)) {
2485
+ if (cap = this.rules.code.exec(src)) {
1807
2486
  src = src.substring(cap[0].length);
1808
2487
  out += '<code>'
1809
2488
  + escape(cap[2], true)
@@ -1812,24 +2491,42 @@ inline.lexer = function(src) {
1812
2491
  }
1813
2492
 
1814
2493
  // br
1815
- if (cap = inline.br.exec(src)) {
2494
+ if (cap = this.rules.br.exec(src)) {
1816
2495
  src = src.substring(cap[0].length);
1817
2496
  out += '<br>';
1818
2497
  continue;
1819
2498
  }
1820
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
+
1821
2509
  // text
1822
- if (cap = inline.text.exec(src)) {
2510
+ if (cap = this.rules.text.exec(src)) {
1823
2511
  src = src.substring(cap[0].length);
1824
2512
  out += escape(cap[0]);
1825
2513
  continue;
1826
2514
  }
2515
+
2516
+ if (src) {
2517
+ throw new
2518
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
2519
+ }
1827
2520
  }
1828
2521
 
1829
2522
  return out;
1830
2523
  };
1831
2524
 
1832
- function outputLink(cap, link) {
2525
+ /**
2526
+ * Compile Link
2527
+ */
2528
+
2529
+ InlineLexer.prototype.outputLink = function(cap, link) {
1833
2530
  if (cap[0][0] !== '!') {
1834
2531
  return '<a href="'
1835
2532
  + escape(link.href)
@@ -1840,7 +2537,7 @@ function outputLink(cap, link) {
1840
2537
  + '"'
1841
2538
  : '')
1842
2539
  + '>'
1843
- + inline.lexer(cap[1])
2540
+ + this.output(cap[1])
1844
2541
  + '</a>';
1845
2542
  } else {
1846
2543
  return '<img src="'
@@ -1855,21 +2552,100 @@ function outputLink(cap, link) {
1855
2552
  : '')
1856
2553
  + '>';
1857
2554
  }
1858
- }
2555
+ };
1859
2556
 
1860
2557
  /**
1861
- * Parsing
2558
+ * Mangle Links
1862
2559
  */
1863
2560
 
1864
- var tokens
1865
- , token;
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
+ }
1866
2574
 
1867
- function next() {
1868
- return token = tokens.pop();
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;
1869
2586
  }
1870
2587
 
1871
- function tok() {
1872
- switch (token.type) {
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) {
1873
2649
  case 'space': {
1874
2650
  return '';
1875
2651
  }
@@ -1878,41 +2654,78 @@ function tok() {
1878
2654
  }
1879
2655
  case 'heading': {
1880
2656
  return '<h'
1881
- + token.depth
2657
+ + this.token.depth
1882
2658
  + '>'
1883
- + inline.lexer(token.text)
2659
+ + this.inline.output(this.token.text)
1884
2660
  + '</h'
1885
- + token.depth
2661
+ + this.token.depth
1886
2662
  + '>\n';
1887
2663
  }
1888
2664
  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;
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;
1894
2670
  }
1895
2671
  }
1896
2672
 
1897
- if (!token.escaped) {
1898
- token.text = escape(token.text, true);
2673
+ if (!this.token.escaped) {
2674
+ this.token.text = escape(this.token.text, true);
1899
2675
  }
1900
2676
 
1901
2677
  return '<pre><code'
1902
- + (token.lang
2678
+ + (this.token.lang
1903
2679
  ? ' class="lang-'
1904
- + token.lang
2680
+ + this.token.lang
1905
2681
  + '"'
1906
2682
  : '')
1907
2683
  + '>'
1908
- + token.text
2684
+ + this.token.text
1909
2685
  + '</code></pre>\n';
1910
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
+ }
1911
2724
  case 'blockquote_start': {
1912
2725
  var body = '';
1913
2726
 
1914
- while (next().type !== 'blockquote_end') {
1915
- body += tok();
2727
+ while (this.next().type !== 'blockquote_end') {
2728
+ body += this.tok();
1916
2729
  }
1917
2730
 
1918
2731
  return '<blockquote>\n'
@@ -1920,11 +2733,11 @@ function tok() {
1920
2733
  + '</blockquote>\n';
1921
2734
  }
1922
2735
  case 'list_start': {
1923
- var type = token.ordered ? 'ol' : 'ul'
2736
+ var type = this.token.ordered ? 'ol' : 'ul'
1924
2737
  , body = '';
1925
2738
 
1926
- while (next().type !== 'list_end') {
1927
- body += tok();
2739
+ while (this.next().type !== 'list_end') {
2740
+ body += this.tok();
1928
2741
  }
1929
2742
 
1930
2743
  return '<'
@@ -1938,10 +2751,10 @@ function tok() {
1938
2751
  case 'list_item_start': {
1939
2752
  var body = '';
1940
2753
 
1941
- while (next().type !== 'list_item_end') {
1942
- body += token.type === 'text'
1943
- ? parseText()
1944
- : tok();
2754
+ while (this.next().type !== 'list_item_end') {
2755
+ body += this.token.type === 'text'
2756
+ ? this.parseText()
2757
+ : this.tok();
1945
2758
  }
1946
2759
 
1947
2760
  return '<li>'
@@ -1951,8 +2764,8 @@ function tok() {
1951
2764
  case 'loose_item_start': {
1952
2765
  var body = '';
1953
2766
 
1954
- while (next().type !== 'list_item_end') {
1955
- body += tok();
2767
+ while (this.next().type !== 'list_item_end') {
2768
+ body += this.tok();
1956
2769
  }
1957
2770
 
1958
2771
  return '<li>'
@@ -1960,51 +2773,22 @@ function tok() {
1960
2773
  + '</li>\n';
1961
2774
  }
1962
2775
  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;
2776
+ return !this.token.pre && !this.options.pedantic
2777
+ ? this.inline.output(this.token.text)
2778
+ : this.token.text;
1969
2779
  }
1970
2780
  case 'paragraph': {
1971
2781
  return '<p>'
1972
- + inline.lexer(token.text)
2782
+ + this.inline.output(this.token.text)
1973
2783
  + '</p>\n';
1974
2784
  }
1975
2785
  case 'text': {
1976
2786
  return '<p>'
1977
- + parseText()
2787
+ + this.parseText()
1978
2788
  + '</p>\n';
1979
2789
  }
1980
2790
  }
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
- }
2791
+ };
2008
2792
 
2009
2793
  /**
2010
2794
  * Helpers
@@ -2019,38 +2803,14 @@ function escape(html, encode) {
2019
2803
  .replace(/'/g, '&#39;');
2020
2804
  }
2021
2805
 
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
2806
  function replace(regex, opt) {
2049
2807
  regex = regex.source;
2050
2808
  opt = opt || '';
2051
2809
  return function self(name, val) {
2052
2810
  if (!name) return new RegExp(regex, opt);
2053
- regex = regex.replace(name, val.source || val);
2811
+ val = val.source || val;
2812
+ val = val.replace(/(^|[^\[])\^/g, '$1');
2813
+ regex = regex.replace(name, val);
2054
2814
  return self;
2055
2815
  };
2056
2816
  }
@@ -2058,80 +2818,78 @@ function replace(regex, opt) {
2058
2818
  function noop() {}
2059
2819
  noop.exec = noop;
2060
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
+
2061
2838
  /**
2062
2839
  * Marked
2063
2840
  */
2064
2841
 
2065
2842
  function marked(src, opt) {
2066
- setOptions(opt);
2067
- return parse(block.lexer(src));
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
+ }
2068
2852
  }
2069
2853
 
2070
2854
  /**
2071
2855
  * Options
2072
2856
  */
2073
2857
 
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
2858
  marked.options =
2104
2859
  marked.setOptions = function(opt) {
2105
- defaults = opt;
2106
- setOptions(opt);
2860
+ marked.defaults = opt;
2107
2861
  return marked;
2108
2862
  };
2109
2863
 
2110
- marked.setOptions({
2864
+ marked.defaults = {
2111
2865
  gfm: true,
2866
+ tables: true,
2867
+ breaks: false,
2112
2868
  pedantic: false,
2113
2869
  sanitize: false,
2870
+ silent: false,
2114
2871
  highlight: null
2115
- });
2872
+ };
2116
2873
 
2117
2874
  /**
2118
2875
  * Expose
2119
2876
  */
2120
2877
 
2121
- marked.parser = function(src, opt) {
2122
- setOptions(opt);
2123
- return parse(src);
2124
- };
2878
+ marked.Parser = Parser;
2879
+ marked.parser = Parser.parse;
2125
2880
 
2126
- marked.lexer = function(src, opt) {
2127
- setOptions(opt);
2128
- return block.lexer(src);
2129
- };
2881
+ marked.Lexer = Lexer;
2882
+ marked.lexer = Lexer.lex;
2883
+
2884
+ marked.InlineLexer = InlineLexer;
2885
+ marked.inlineLexer = InlineLexer.output;
2130
2886
 
2131
2887
  marked.parse = marked;
2132
2888
 
2133
2889
  if (typeof module !== 'undefined') {
2134
2890
  module.exports = marked;
2891
+ } else if (typeof define === 'function' && define.amd) {
2892
+ define(function() { return marked; });
2135
2893
  } else {
2136
2894
  this.marked = marked;
2137
2895
  }