epic-editor-rails 0.2.0 → 0.2.2

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