epic-editor-rails 0.2.3 → 0.2.4

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: 1fa66f152a4fff35abcf1522771aac3785451cb5
4
- data.tar.gz: a8cbe9f6a7894d92cbe86b3b428fcc5cf0a15a27
3
+ metadata.gz: 5a5a65f525f4a14946a36e5abbc6998b6bf0733b
4
+ data.tar.gz: d4d27f27b14f36b91e5ad7faa55594cd838c2ab2
5
5
  SHA512:
6
- metadata.gz: 0d1c3b7db1838b999b9a84f9bd5da77bf7ce713b9106a1bdf6f67409cb213cf2001d09f7b40e5c88d25b29d94446a753c050f3a74ff3ff78999752dea28f641e
7
- data.tar.gz: 2fecc7cc601c1f37917d29d6398ff0a97ace9427017c0d803b61bab478346d2d7a274b26e19c668f8d41112f2e30ea2d10d74ec93ebe291562dc90c6aebc6f60
6
+ metadata.gz: d089e2dda58ac142bcd69e4381a825a0a9535a421c7434fc2446a2df0318fd21ad71b928b4528c477967b1bdca3855330f1d1d1522fb47c9a8d778c2c821b3b8
7
+ data.tar.gz: b278a969811441a20079250424d8b7cf128030a16d73faa9eced5cdc1364c472b208055c530e9de710e5ec4ccd8ba445067d8a2f6878938dbc4f9f0e69d3d88f
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- epic-editor-rails v0.2.3
1
+ epic-editor-rails v0.2.4
2
2
  =================
3
3
  Gemfile: ```gem 'epic-editor-rails'```
4
4
  Install: ```$ bundle install ```
@@ -18,5 +18,9 @@ application.css.(scss):
18
18
  @import 'editor/epic-light';
19
19
  ```
20
20
 
21
- EpicEditor v0.2.2
21
+ EpicEditor v0.2.3
22
22
  http://epiceditor.com/
23
+
24
+
25
+ [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/zethussuen/epic-editor-rails/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
26
+
data/Rakefile CHANGED
@@ -1,3 +1,51 @@
1
- require File.expand_path('../config/application', __FILE__)
1
+ require 'open-uri'
2
2
 
3
- EpicEditorRails::Application.load_tasks
3
+ begin
4
+ require File.expand_path('../config/application', __FILE__)
5
+ EpicEditorRails::Application.load_tasks
6
+ rescue LoadError
7
+ end
8
+
9
+ # rake update[param]
10
+ # param can be a tag name, a branch name, or a commit hash on https://github.com/OscarGodson/EpicEditor
11
+ # It defaults to 'develop' (ie, the develop branch)
12
+ task :update, [:arg1] do |t, args|
13
+ version = args[:arg1] || 'develop'
14
+ puts "Updating to EpicEditor #{version}"
15
+
16
+ # version can be a commit hash, a branch name, or a tag name - github will handle them all the same.
17
+ base_url = "https://raw.githubusercontent.com/OscarGodson/EpicEditor/#{version}/"
18
+
19
+ js_url = "#{base_url}epiceditor/js/epiceditor.js"
20
+ puts "copying #{js_url}"
21
+ js_content = open(js_url).read
22
+
23
+ puts 'Replacing base path'
24
+ js_content.gsub!(/basePath: 'epiceditor'/, "basePath: ''")
25
+
26
+ # Use asset pipeline version of css files.
27
+ puts 'Substituting asset pipeline paths'
28
+ js_content.gsub!(/'\/themes\/(\w.*)\/(\w.*)\.css'/, "'<%= asset_path(\"\\1/\\2.css\") %>'")
29
+
30
+ open('./vendor/assets/javascripts/epiceditor.js.erb', 'wb') { |f| f << js_content }
31
+
32
+ remote_css_base = "#{base_url}epiceditor/themes/"
33
+ local_css_base = './vendor/assets/stylesheets/'
34
+ css_files = [
35
+ 'base/epiceditor.css',
36
+ 'editor/epic-dark.css',
37
+ 'editor/epic-light.css',
38
+ 'preview/bartik.css',
39
+ 'preview/github.css',
40
+ 'preview/preview-dark.css',
41
+ ]
42
+
43
+ css_files.each do |css_file|
44
+ remote_path = remote_css_base + css_file
45
+ local_path = local_css_base + css_file
46
+ css_content = open(remote_path).read
47
+
48
+ puts "copying #{remote_path}"
49
+ open(local_path, 'wb') { |f| f << css_content }
50
+ end
51
+ end
@@ -1,7 +1,7 @@
1
1
  module Epic
2
2
  module Editor
3
3
  module Rails
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
6
6
  end
7
7
  end
@@ -13,7 +13,7 @@
13
13
  function _applyAttrs(context, attrs) {
14
14
  for (var attr in attrs) {
15
15
  if (attrs.hasOwnProperty(attr)) {
16
- context[attr] = attrs[attr];
16
+ context.setAttribute(attr, attrs[attr]);
17
17
  }
18
18
  }
19
19
  }
@@ -337,7 +337,7 @@
337
337
  }
338
338
  , theme: { base: '<%= asset_path("base/epiceditor.css") %>'
339
339
  , preview: '<%= asset_path("preview/github.css") %>'
340
- , editor: '<%= asset_path("editor/epic-light.css") %>'
340
+ , editor: '<%= asset_path("editor/epic-dark.css") %>'
341
341
  }
342
342
  , focusOnLoad: false
343
343
  , shortcut: { modifier: 18 // alt keycode
@@ -407,6 +407,16 @@
407
407
  else if (typeof self.settings.container == 'object') {
408
408
  self.element = self.settings.container;
409
409
  }
410
+
411
+ if (typeof self.settings.textarea == 'undefined' && typeof self.element != 'undefined') {
412
+ var textareas = self.element.getElementsByTagName('textarea');
413
+ if (textareas.length > 0) {
414
+ self.settings.textarea = textareas[0];
415
+ _applyStyles(self.settings.textarea, {
416
+ display: 'none'
417
+ });
418
+ }
419
+ }
410
420
 
411
421
  // Figure out the file name. If no file name is given we'll use the ID.
412
422
  // If there's no ID either we'll use a namespaced file name that's incremented
@@ -567,15 +577,21 @@
567
577
  };
568
578
 
569
579
  // Write an iframe and then select it for the editor
570
- self.element.innerHTML = '<iframe scrolling="no" frameborder="0" id= "' + self._instanceId + '"></iframe>';
580
+ iframeElement = document.createElement('iframe');
581
+ _applyAttrs(iframeElement, {
582
+ scrolling: 'no',
583
+ frameborder: 0,
584
+ id: self._instanceId
585
+ });
586
+
587
+
588
+ self.element.appendChild(iframeElement);
571
589
 
572
590
  // Because browsers add things like invisible padding and margins and stuff
573
591
  // to iframes, we need to set manually set the height so that the height
574
592
  // doesn't keep increasing (by 2px?) every time reflow() is called.
575
593
  // FIXME: Figure out how to fix this without setting this
576
594
  self.element.style.height = self.element.offsetHeight + 'px';
577
-
578
- iframeElement = document.getElementById(self._instanceId);
579
595
 
580
596
  // Store a reference to the iframeElement itself
581
597
  self.iframeElement = iframeElement;
@@ -684,17 +700,20 @@
684
700
 
685
701
  // TODO: Move into fullscreen setup function (_setupFullscreen)
686
702
  _elementStates = {}
687
- self._goFullscreen = function (el) {
703
+ self._goFullscreen = function (el, callback) {
704
+ callback = callback || function () {};
705
+ var wait = 0;
688
706
  this._fixScrollbars('auto');
689
707
 
690
708
  if (self.is('fullscreen')) {
691
- self._exitFullscreen(el);
709
+ self._exitFullscreen(el, callback);
692
710
  return;
693
711
  }
694
712
 
695
713
  if (nativeFs) {
696
714
  if (nativeFsWebkit) {
697
715
  el.webkitRequestFullScreen();
716
+ wait = 750;
698
717
  }
699
718
  else if (nativeFsMoz) {
700
719
  el.mozRequestFullScreen();
@@ -706,85 +725,99 @@
706
725
 
707
726
  _isInEdit = self.is('edit');
708
727
 
709
- // Set the state of EE in fullscreen
710
- // We set edit and preview to true also because they're visible
711
- // we might want to allow fullscreen edit mode without preview (like a "zen" mode)
712
- self._eeState.fullscreen = true;
713
- self._eeState.edit = true;
714
- self._eeState.preview = true;
715
728
 
716
- // Cache calculations
717
- var windowInnerWidth = window.innerWidth
718
- , windowInnerHeight = window.innerHeight
719
- , windowOuterWidth = window.outerWidth
720
- , windowOuterHeight = window.outerHeight;
729
+ // Why does this need to be in a randomly "750"ms setTimeout? WebKit's
730
+ // implementation of fullscreen seem to trigger the webkitfullscreenchange
731
+ // event _after_ everything is done. Instead, it triggers _during_ the
732
+ // transition. This means calculations of what's half, 100%, etc are wrong
733
+ // so to combat this we throw down the hammer with a setTimeout and wait
734
+ // to trigger our calculation code.
735
+ // See: https://code.google.com/p/chromium/issues/detail?id=181116
736
+ setTimeout(function () {
737
+ // Set the state of EE in fullscreen
738
+ // We set edit and preview to true also because they're visible
739
+ // we might want to allow fullscreen edit mode without preview (like a "zen" mode)
740
+ self._eeState.fullscreen = true;
741
+ self._eeState.edit = true;
742
+ self._eeState.preview = true;
743
+
744
+ // Cache calculations
745
+ var windowInnerWidth = window.innerWidth
746
+ , windowInnerHeight = window.innerHeight
747
+ , windowOuterWidth = window.outerWidth
748
+ , windowOuterHeight = window.outerHeight;
749
+
750
+ // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66)
751
+ if (!nativeFs) {
752
+ windowOuterHeight = window.innerHeight;
753
+ }
721
754
 
722
- // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66)
723
- if (!nativeFs) {
724
- windowOuterHeight = window.innerHeight;
725
- }
726
-
727
- // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper
728
- // the editor's width wont be the same as before
729
- _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', {
730
- 'width': windowOuterWidth / 2 + 'px'
731
- , 'height': windowOuterHeight + 'px'
732
- , 'float': 'left' // Most browsers
733
- , 'cssFloat': 'left' // FF
734
- , 'styleFloat': 'left' // Older IEs
735
- , 'display': 'block'
736
- , 'position': 'static'
737
- , 'left': ''
738
- });
755
+ // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper
756
+ // the editor's width wont be the same as before
757
+ _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', {
758
+ 'width': windowOuterWidth / 2 + 'px'
759
+ , 'height': windowOuterHeight + 'px'
760
+ , 'float': 'left' // Most browsers
761
+ , 'cssFloat': 'left' // FF
762
+ , 'styleFloat': 'left' // Older IEs
763
+ , 'display': 'block'
764
+ , 'position': 'static'
765
+ , 'left': ''
766
+ });
739
767
 
740
- // the previewer
741
- _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', {
742
- 'width': windowOuterWidth / 2 + 'px'
743
- , 'height': windowOuterHeight + 'px'
744
- , 'float': 'right' // Most browsers
745
- , 'cssFloat': 'right' // FF
746
- , 'styleFloat': 'right' // Older IEs
747
- , 'display': 'block'
748
- , 'position': 'static'
749
- , 'left': ''
750
- });
768
+ // the previewer
769
+ _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', {
770
+ 'width': windowOuterWidth / 2 + 'px'
771
+ , 'height': windowOuterHeight + 'px'
772
+ , 'float': 'right' // Most browsers
773
+ , 'cssFloat': 'right' // FF
774
+ , 'styleFloat': 'right' // Older IEs
775
+ , 'display': 'block'
776
+ , 'position': 'static'
777
+ , 'left': ''
778
+ });
751
779
 
752
- // Setup the containing element CSS for fullscreen
753
- _elementStates.element = _saveStyleState(self.element, 'save', {
754
- 'position': 'fixed'
755
- , 'top': '0'
756
- , 'left': '0'
757
- , 'width': '100%'
758
- , 'z-index': '9999' // Most browsers
759
- , 'zIndex': '9999' // Firefox
760
- , 'border': 'none'
761
- , 'margin': '0'
762
- // Should use the base styles background!
763
- , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below
764
- , 'height': windowInnerHeight + 'px'
765
- });
780
+ // Setup the containing element CSS for fullscreen
781
+ _elementStates.element = _saveStyleState(self.element, 'save', {
782
+ 'position': 'fixed'
783
+ , 'top': '0'
784
+ , 'left': '0'
785
+ , 'width': '100%'
786
+ , 'z-index': '9999' // Most browsers
787
+ , 'zIndex': '9999' // Firefox
788
+ , 'border': 'none'
789
+ , 'margin': '0'
790
+ // Should use the base styles background!
791
+ , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below
792
+ , 'height': windowInnerHeight + 'px'
793
+ });
766
794
 
767
- // The iframe element
768
- _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', {
769
- 'width': windowOuterWidth + 'px'
770
- , 'height': windowInnerHeight + 'px'
771
- });
795
+ // The iframe element
796
+ _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', {
797
+ 'width': windowOuterWidth + 'px'
798
+ , 'height': windowInnerHeight + 'px'
799
+ });
772
800
 
773
- // ...Oh, and hide the buttons and prevent scrolling
774
- utilBtns.style.visibility = 'hidden';
801
+ // ...Oh, and hide the buttons and prevent scrolling
802
+ utilBtns.style.visibility = 'hidden';
775
803
 
776
- if (!nativeFs) {
777
- document.body.style.overflow = 'hidden';
778
- }
804
+ if (!nativeFs) {
805
+ document.body.style.overflow = 'hidden';
806
+ }
779
807
 
780
- self.preview();
808
+ self.preview();
781
809
 
782
- self.focus();
810
+ self.focus();
811
+
812
+ self.emit('fullscreenenter');
813
+
814
+ callback.call(self);
815
+ }, wait);
783
816
 
784
- self.emit('fullscreenenter');
785
817
  };
786
818
 
787
- self._exitFullscreen = function (el) {
819
+ self._exitFullscreen = function (el, callback) {
820
+ callback = callback || function () {};
788
821
  this._fixScrollbars();
789
822
 
790
823
  _saveStyleState(self.element, 'apply', _elementStates.element);
@@ -831,6 +864,8 @@
831
864
  self.reflow();
832
865
 
833
866
  self.emit('fullscreenexit');
867
+
868
+ callback.call(self);
834
869
  };
835
870
 
836
871
  // This setups up live previews by triggering preview() IF in fullscreen on keyup
@@ -926,6 +961,7 @@
926
961
  function shortcutHandler(e) {
927
962
  if (e.keyCode == self.settings.shortcut.modifier) { isMod = true } // check for modifier press(default is alt key), save to var
928
963
  if (e.keyCode == 17) { isCtrl = true } // check for ctrl/cmnd press, in order to catch ctrl/cmnd + s
964
+ if (e.keyCode == 18) { isCtrl = false }
929
965
 
930
966
  // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview
931
967
  if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) {
@@ -1113,7 +1149,6 @@
1113
1149
 
1114
1150
  EpicEditor.prototype._setupTextareaSync = function () {
1115
1151
  var self = this
1116
- , textareaFileName = self.settings.file.name
1117
1152
  , _syncTextarea;
1118
1153
 
1119
1154
  // Even if autoSave is false, we want to make sure to keep the textarea synced
@@ -1132,7 +1167,10 @@
1132
1167
  // This only happens for draft files. Probably has something to do with
1133
1168
  // the fact draft files haven't been saved by the time this is called.
1134
1169
  // TODO: Add test for this case.
1135
- self._textareaElement.value = self.exportFile(textareaFileName, 'text', true) || self.settings.file.defaultContent;
1170
+ // Get the file.name each time as it can change. DO NOT save this to a
1171
+ // var outside of this closure or the editor will stop syncing when the
1172
+ // file is changed with importFile or open.
1173
+ self._textareaElement.value = self.exportFile(self.settings.file.name, 'text', true) || self.settings.file.defaultContent;
1136
1174
  }
1137
1175
 
1138
1176
  if (typeof self.settings.textarea == 'string') {
@@ -1156,7 +1194,7 @@
1156
1194
  // If the developer wants drafts to be recoverable they should check if
1157
1195
  // the local file in localStorage's modified date is newer than the server.
1158
1196
  if (self._textareaElement.value !== '') {
1159
- self.importFile(textareaFileName, self._textareaElement.value);
1197
+ self.importFile(self.settings.file.name, self._textareaElement.value);
1160
1198
 
1161
1199
  // manually save draft after import so there is no delay between the
1162
1200
  // import and exporting in _syncTextarea. Without this, _syncTextarea
@@ -1169,6 +1207,8 @@
1169
1207
 
1170
1208
  // Make sure to keep it updated
1171
1209
  self.on('__update', _syncTextarea);
1210
+ self.on('__create', _syncTextarea);
1211
+ self.on('__save', _syncTextarea);
1172
1212
  }
1173
1213
 
1174
1214
  /**
@@ -1208,7 +1248,6 @@
1208
1248
  callback = callback || function () {};
1209
1249
 
1210
1250
  if (self.settings.textarea) {
1211
- self._textareaElement.value = "";
1212
1251
  self.removeListener('__update');
1213
1252
  }
1214
1253
 
@@ -1340,9 +1379,13 @@
1340
1379
  * Puts the editor into fullscreen mode
1341
1380
  * @returns {object} EpicEditor will be returned
1342
1381
  */
1343
- EpicEditor.prototype.enterFullscreen = function () {
1344
- if (this.is('fullscreen')) { return this; }
1345
- this._goFullscreen(this.iframeElement);
1382
+ EpicEditor.prototype.enterFullscreen = function (callback) {
1383
+ callback = callback || function () {};
1384
+ if (this.is('fullscreen')) {
1385
+ callback.call(this);
1386
+ return this;
1387
+ }
1388
+ this._goFullscreen(this.iframeElement, callback);
1346
1389
  return this;
1347
1390
  }
1348
1391
 
@@ -1350,9 +1393,13 @@
1350
1393
  * Closes fullscreen mode if opened
1351
1394
  * @returns {object} EpicEditor will be returned
1352
1395
  */
1353
- EpicEditor.prototype.exitFullscreen = function () {
1354
- if (!this.is('fullscreen')) { return this; }
1355
- this._exitFullscreen(this.iframeElement);
1396
+ EpicEditor.prototype.exitFullscreen = function (callback) {
1397
+ callback = callback || function () {};
1398
+ if (!this.is('fullscreen')) {
1399
+ callback.call(this);
1400
+ return this;
1401
+ }
1402
+ this._exitFullscreen(this.iframeElement, callback);
1356
1403
  return this;
1357
1404
  }
1358
1405
 
@@ -1461,6 +1508,7 @@
1461
1508
  var self = this
1462
1509
  , storage
1463
1510
  , isUpdate = false
1511
+ , isNew = false
1464
1512
  , file = self.settings.file.name
1465
1513
  , previewDraftName = ''
1466
1514
  , data = this._storage[previewDraftName + self.settings.localStorageName]
@@ -1482,6 +1530,7 @@
1482
1530
  // If the file doesn't exist we need to create it
1483
1531
  if (storage[file] === undefined) {
1484
1532
  storage[file] = self._defaultFileSchema();
1533
+ isNew = true;
1485
1534
  }
1486
1535
 
1487
1536
  // If it does, we need to check if the content is different and
@@ -1498,10 +1547,17 @@
1498
1547
  storage[file].content = content;
1499
1548
  this._storage[previewDraftName + self.settings.localStorageName] = JSON.stringify(storage);
1500
1549
 
1501
- // After the content is actually changed, emit update so it emits the updated content
1550
+ // If it's a new file, send a create event as well as a private one for
1551
+ // use internally.
1552
+ if (isNew) {
1553
+ self.emit('create');
1554
+ self.emit('__create');
1555
+ }
1556
+
1557
+ // After the content is actually changed, emit update so it emits the
1558
+ // updated content. Also send a private event for interal use.
1502
1559
  if (isUpdate) {
1503
1560
  self.emit('update');
1504
- // Emit a private update event so it can't get accidentally removed
1505
1561
  self.emit('__update');
1506
1562
  }
1507
1563
 
@@ -1510,6 +1566,7 @@
1510
1566
  }
1511
1567
  else if (!_isPreviewDraft) {
1512
1568
  this.emit('save');
1569
+ self.emit('__save');
1513
1570
  }
1514
1571
  }
1515
1572
 
@@ -1563,26 +1620,17 @@
1563
1620
  * @returns {object} EpicEditor will be returned
1564
1621
  */
1565
1622
  EpicEditor.prototype.importFile = function (name, content, kind, meta) {
1566
- var self = this
1567
- , isNew = false;
1623
+ var self = this;
1568
1624
 
1569
1625
  name = name || self.settings.file.name;
1570
1626
  content = content || '';
1571
1627
  kind = kind || 'md';
1572
1628
  meta = meta || {};
1573
1629
 
1574
- if (JSON.parse(this._storage[self.settings.localStorageName])[name] === undefined) {
1575
- isNew = true;
1576
- }
1577
-
1578
1630
  // Set our current file to the new file and update the content
1579
1631
  self.settings.file.name = name;
1580
1632
  _setText(self.editor, content);
1581
1633
 
1582
- if (isNew) {
1583
- self.emit('create');
1584
- }
1585
-
1586
1634
  self.save();
1587
1635
 
1588
1636
  if (self.is('fullscreen')) {
@@ -1843,12 +1891,15 @@
1843
1891
  // Used to store information to be shared across editors
1844
1892
  EpicEditor._data = {};
1845
1893
 
1846
- window.EpicEditor = EpicEditor;
1894
+ if (typeof window.define === 'function' && window.define.amd) {
1895
+ window.define(function () { return EpicEditor; });
1896
+ } else {
1897
+ window.EpicEditor = EpicEditor;
1898
+ }
1847
1899
  })(window);
1848
-
1849
1900
  /**
1850
1901
  * marked - a markdown parser
1851
- * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
1902
+ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
1852
1903
  * https://github.com/chjj/marked
1853
1904
  */
1854
1905
 
@@ -1865,13 +1916,13 @@ var block = {
1865
1916
  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
1866
1917
  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
1867
1918
  nptable: noop,
1868
- lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
1869
- blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
1870
- list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
1871
- html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
1872
- def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
1919
+ lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
1920
+ blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
1921
+ list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
1922
+ html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,
1923
+ def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
1873
1924
  table: noop,
1874
- paragraph: /^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/,
1925
+ paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
1875
1926
  text: /^[^\n]+/
1876
1927
  };
1877
1928
 
@@ -1883,13 +1934,18 @@ block.item = replace(block.item, 'gm')
1883
1934
 
1884
1935
  block.list = replace(block.list)
1885
1936
  (/bull/g, block.bullet)
1886
- ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
1937
+ ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))')
1938
+ ('def', '\\n+(?=' + block.def.source + ')')
1939
+ ();
1940
+
1941
+ block.blockquote = replace(block.blockquote)
1942
+ ('def', block.def)
1887
1943
  ();
1888
1944
 
1889
1945
  block._tag = '(?!(?:'
1890
1946
  + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
1891
1947
  + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
1892
- + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b';
1948
+ + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b';
1893
1949
 
1894
1950
  block.html = replace(block.html)
1895
1951
  ('comment', /<!--[\s\S]*?-->/)
@@ -1918,12 +1974,14 @@ block.normal = merge({}, block);
1918
1974
  */
1919
1975
 
1920
1976
  block.gfm = merge({}, block.normal, {
1921
- fences: /^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
1977
+ fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
1922
1978
  paragraph: /^/
1923
1979
  });
1924
1980
 
1925
1981
  block.gfm.paragraph = replace(block.paragraph)
1926
- ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|')
1982
+ ('(?!', '(?!'
1983
+ + block.gfm.fences.source.replace('\\1', '\\2') + '|'
1984
+ + block.list.source.replace('\\1', '\\3') + '|')
1927
1985
  ();
1928
1986
 
1929
1987
  /**
@@ -1987,11 +2045,13 @@ Lexer.prototype.lex = function(src) {
1987
2045
  * Lexing
1988
2046
  */
1989
2047
 
1990
- Lexer.prototype.token = function(src, top) {
2048
+ Lexer.prototype.token = function(src, top, bq) {
1991
2049
  var src = src.replace(/^ +$/gm, '')
1992
2050
  , next
1993
2051
  , loose
1994
2052
  , cap
2053
+ , bull
2054
+ , b
1995
2055
  , item
1996
2056
  , space
1997
2057
  , i
@@ -2108,7 +2168,7 @@ Lexer.prototype.token = function(src, top) {
2108
2168
  // Pass `top` to keep the current
2109
2169
  // "toplevel" state. This is exactly
2110
2170
  // how markdown.pl works.
2111
- this.token(cap, top);
2171
+ this.token(cap, top, true);
2112
2172
 
2113
2173
  this.tokens.push({
2114
2174
  type: 'blockquote_end'
@@ -2120,10 +2180,11 @@ Lexer.prototype.token = function(src, top) {
2120
2180
  // list
2121
2181
  if (cap = this.rules.list.exec(src)) {
2122
2182
  src = src.substring(cap[0].length);
2183
+ bull = cap[2];
2123
2184
 
2124
2185
  this.tokens.push({
2125
2186
  type: 'list_start',
2126
- ordered: isFinite(cap[2])
2187
+ ordered: bull.length > 1
2127
2188
  });
2128
2189
 
2129
2190
  // Get each top-level item.
@@ -2150,12 +2211,22 @@ Lexer.prototype.token = function(src, top) {
2150
2211
  : item.replace(/^ {1,4}/gm, '');
2151
2212
  }
2152
2213
 
2214
+ // Determine whether the next list item belongs here.
2215
+ // Backpedal if it does not belong in this list.
2216
+ if (this.options.smartLists && i !== l - 1) {
2217
+ b = block.bullet.exec(cap[i + 1])[0];
2218
+ if (bull !== b && !(bull.length > 1 && b.length > 1)) {
2219
+ src = cap.slice(i + 1).join('\n') + src;
2220
+ i = l - 1;
2221
+ }
2222
+ }
2223
+
2153
2224
  // Determine whether item is loose or not.
2154
2225
  // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
2155
2226
  // for discount behavior.
2156
2227
  loose = next || /\n\n(?!\s*$)/.test(item);
2157
2228
  if (i !== l - 1) {
2158
- next = item[item.length-1] === '\n';
2229
+ next = item.charAt(item.length - 1) === '\n';
2159
2230
  if (!loose) loose = next;
2160
2231
  }
2161
2232
 
@@ -2166,7 +2237,7 @@ Lexer.prototype.token = function(src, top) {
2166
2237
  });
2167
2238
 
2168
2239
  // Recurse.
2169
- this.token(item, false);
2240
+ this.token(item, false, bq);
2170
2241
 
2171
2242
  this.tokens.push({
2172
2243
  type: 'list_item_end'
@@ -2187,14 +2258,14 @@ Lexer.prototype.token = function(src, top) {
2187
2258
  type: this.options.sanitize
2188
2259
  ? 'paragraph'
2189
2260
  : 'html',
2190
- pre: cap[1] === 'pre',
2261
+ pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',
2191
2262
  text: cap[0]
2192
2263
  });
2193
2264
  continue;
2194
2265
  }
2195
2266
 
2196
2267
  // def
2197
- if (top && (cap = this.rules.def.exec(src))) {
2268
+ if ((!bq && top) && (cap = this.rules.def.exec(src))) {
2198
2269
  src = src.substring(cap[0].length);
2199
2270
  this.tokens.links[cap[1].toLowerCase()] = {
2200
2271
  href: cap[2],
@@ -2242,7 +2313,9 @@ Lexer.prototype.token = function(src, top) {
2242
2313
  src = src.substring(cap[0].length);
2243
2314
  this.tokens.push({
2244
2315
  type: 'paragraph',
2245
- text: cap[0]
2316
+ text: cap[1].charAt(cap[1].length - 1) === '\n'
2317
+ ? cap[1].slice(0, -1)
2318
+ : cap[1]
2246
2319
  });
2247
2320
  continue;
2248
2321
  }
@@ -2272,7 +2345,7 @@ Lexer.prototype.token = function(src, top) {
2272
2345
  */
2273
2346
 
2274
2347
  var inline = {
2275
- escape: /^\\([\\`*{}\[\]()#+\-.!_>|])/,
2348
+ escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
2276
2349
  autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
2277
2350
  url: noop,
2278
2351
  tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
@@ -2281,14 +2354,14 @@ var inline = {
2281
2354
  nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
2282
2355
  strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
2283
2356
  em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
2284
- code: /^(`+)([\s\S]*?[^`])\1(?!`)/,
2357
+ code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
2285
2358
  br: /^ {2,}\n(?!\s*$)/,
2286
2359
  del: noop,
2287
2360
  text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
2288
2361
  };
2289
2362
 
2290
- inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
2291
- inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
2363
+ inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
2364
+ inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
2292
2365
 
2293
2366
  inline.link = replace(inline.link)
2294
2367
  ('inside', inline._inside)
@@ -2319,9 +2392,9 @@ inline.pedantic = merge({}, inline.normal, {
2319
2392
  */
2320
2393
 
2321
2394
  inline.gfm = merge({}, inline.normal, {
2322
- escape: replace(inline.escape)('])', '~])')(),
2323
- url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,
2324
- del: /^~{2,}([\s\S]+?)~{2,}/,
2395
+ escape: replace(inline.escape)('])', '~|])')(),
2396
+ url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
2397
+ del: /^~~(?=\S)([\s\S]*?\S)~~/,
2325
2398
  text: replace(inline.text)
2326
2399
  (']|', '~]|')
2327
2400
  ('|', '|https?://|')
@@ -2345,6 +2418,8 @@ function InlineLexer(links, options) {
2345
2418
  this.options = options || marked.defaults;
2346
2419
  this.links = links;
2347
2420
  this.rules = inline.normal;
2421
+ this.renderer = this.options.renderer || new Renderer;
2422
+ this.renderer.options = this.options;
2348
2423
 
2349
2424
  if (!this.links) {
2350
2425
  throw new
@@ -2372,8 +2447,8 @@ InlineLexer.rules = inline;
2372
2447
  * Static Lexing/Compiling Method
2373
2448
  */
2374
2449
 
2375
- InlineLexer.output = function(src, links, opt) {
2376
- var inline = new InlineLexer(links, opt);
2450
+ InlineLexer.output = function(src, links, options) {
2451
+ var inline = new InlineLexer(links, options);
2377
2452
  return inline.output(src);
2378
2453
  };
2379
2454
 
@@ -2400,7 +2475,7 @@ InlineLexer.prototype.output = function(src) {
2400
2475
  if (cap = this.rules.autolink.exec(src)) {
2401
2476
  src = src.substring(cap[0].length);
2402
2477
  if (cap[2] === '@') {
2403
- text = cap[1][6] === ':'
2478
+ text = cap[1].charAt(6) === ':'
2404
2479
  ? this.mangle(cap[1].substring(7))
2405
2480
  : this.mangle(cap[1]);
2406
2481
  href = this.mangle('mailto:') + text;
@@ -2408,29 +2483,26 @@ InlineLexer.prototype.output = function(src) {
2408
2483
  text = escape(cap[1]);
2409
2484
  href = text;
2410
2485
  }
2411
- out += '<a href="'
2412
- + href
2413
- + '">'
2414
- + text
2415
- + '</a>';
2486
+ out += this.renderer.link(href, null, text);
2416
2487
  continue;
2417
2488
  }
2418
2489
 
2419
2490
  // url (gfm)
2420
- if (cap = this.rules.url.exec(src)) {
2491
+ if (!this.inLink && (cap = this.rules.url.exec(src))) {
2421
2492
  src = src.substring(cap[0].length);
2422
2493
  text = escape(cap[1]);
2423
2494
  href = text;
2424
- out += '<a href="'
2425
- + href
2426
- + '">'
2427
- + text
2428
- + '</a>';
2495
+ out += this.renderer.link(href, null, text);
2429
2496
  continue;
2430
2497
  }
2431
2498
 
2432
2499
  // tag
2433
2500
  if (cap = this.rules.tag.exec(src)) {
2501
+ if (!this.inLink && /^<a /i.test(cap[0])) {
2502
+ this.inLink = true;
2503
+ } else if (this.inLink && /^<\/a>/i.test(cap[0])) {
2504
+ this.inLink = false;
2505
+ }
2434
2506
  src = src.substring(cap[0].length);
2435
2507
  out += this.options.sanitize
2436
2508
  ? escape(cap[0])
@@ -2441,10 +2513,12 @@ InlineLexer.prototype.output = function(src) {
2441
2513
  // link
2442
2514
  if (cap = this.rules.link.exec(src)) {
2443
2515
  src = src.substring(cap[0].length);
2516
+ this.inLink = true;
2444
2517
  out += this.outputLink(cap, {
2445
2518
  href: cap[2],
2446
2519
  title: cap[3]
2447
2520
  });
2521
+ this.inLink = false;
2448
2522
  continue;
2449
2523
  }
2450
2524
 
@@ -2455,61 +2529,55 @@ InlineLexer.prototype.output = function(src) {
2455
2529
  link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
2456
2530
  link = this.links[link.toLowerCase()];
2457
2531
  if (!link || !link.href) {
2458
- out += cap[0][0];
2532
+ out += cap[0].charAt(0);
2459
2533
  src = cap[0].substring(1) + src;
2460
2534
  continue;
2461
2535
  }
2536
+ this.inLink = true;
2462
2537
  out += this.outputLink(cap, link);
2538
+ this.inLink = false;
2463
2539
  continue;
2464
2540
  }
2465
2541
 
2466
2542
  // strong
2467
2543
  if (cap = this.rules.strong.exec(src)) {
2468
2544
  src = src.substring(cap[0].length);
2469
- out += '<strong>'
2470
- + this.output(cap[2] || cap[1])
2471
- + '</strong>';
2545
+ out += this.renderer.strong(this.output(cap[2] || cap[1]));
2472
2546
  continue;
2473
2547
  }
2474
2548
 
2475
2549
  // em
2476
2550
  if (cap = this.rules.em.exec(src)) {
2477
2551
  src = src.substring(cap[0].length);
2478
- out += '<em>'
2479
- + this.output(cap[2] || cap[1])
2480
- + '</em>';
2552
+ out += this.renderer.em(this.output(cap[2] || cap[1]));
2481
2553
  continue;
2482
2554
  }
2483
2555
 
2484
2556
  // code
2485
2557
  if (cap = this.rules.code.exec(src)) {
2486
2558
  src = src.substring(cap[0].length);
2487
- out += '<code>'
2488
- + escape(cap[2], true)
2489
- + '</code>';
2559
+ out += this.renderer.codespan(escape(cap[2], true));
2490
2560
  continue;
2491
2561
  }
2492
2562
 
2493
2563
  // br
2494
2564
  if (cap = this.rules.br.exec(src)) {
2495
2565
  src = src.substring(cap[0].length);
2496
- out += '<br>';
2566
+ out += this.renderer.br();
2497
2567
  continue;
2498
2568
  }
2499
2569
 
2500
2570
  // del (gfm)
2501
2571
  if (cap = this.rules.del.exec(src)) {
2502
2572
  src = src.substring(cap[0].length);
2503
- out += '<del>'
2504
- + this.output(cap[1])
2505
- + '</del>';
2573
+ out += this.renderer.del(this.output(cap[1]));
2506
2574
  continue;
2507
2575
  }
2508
2576
 
2509
2577
  // text
2510
2578
  if (cap = this.rules.text.exec(src)) {
2511
2579
  src = src.substring(cap[0].length);
2512
- out += escape(cap[0]);
2580
+ out += escape(this.smartypants(cap[0]));
2513
2581
  continue;
2514
2582
  }
2515
2583
 
@@ -2527,31 +2595,33 @@ InlineLexer.prototype.output = function(src) {
2527
2595
  */
2528
2596
 
2529
2597
  InlineLexer.prototype.outputLink = function(cap, link) {
2530
- if (cap[0][0] !== '!') {
2531
- return '<a href="'
2532
- + escape(link.href)
2533
- + '"'
2534
- + (link.title
2535
- ? ' title="'
2536
- + escape(link.title)
2537
- + '"'
2538
- : '')
2539
- + '>'
2540
- + this.output(cap[1])
2541
- + '</a>';
2542
- } else {
2543
- return '<img src="'
2544
- + escape(link.href)
2545
- + '" alt="'
2546
- + escape(cap[1])
2547
- + '"'
2548
- + (link.title
2549
- ? ' title="'
2550
- + escape(link.title)
2551
- + '"'
2552
- : '')
2553
- + '>';
2554
- }
2598
+ var href = escape(link.href)
2599
+ , title = link.title ? escape(link.title) : null;
2600
+
2601
+ return cap[0].charAt(0) !== '!'
2602
+ ? this.renderer.link(href, title, this.output(cap[1]))
2603
+ : this.renderer.image(href, title, escape(cap[1]));
2604
+ };
2605
+
2606
+ /**
2607
+ * Smartypants Transformations
2608
+ */
2609
+
2610
+ InlineLexer.prototype.smartypants = function(text) {
2611
+ if (!this.options.smartypants) return text;
2612
+ return text
2613
+ // em-dashes
2614
+ .replace(/--/g, '\u2014')
2615
+ // opening singles
2616
+ .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
2617
+ // closing singles & apostrophes
2618
+ .replace(/'/g, '\u2019')
2619
+ // opening doubles
2620
+ .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
2621
+ // closing doubles
2622
+ .replace(/"/g, '\u201d')
2623
+ // ellipses
2624
+ .replace(/\.{3}/g, '\u2026');
2555
2625
  };
2556
2626
 
2557
2627
  /**
@@ -2575,6 +2645,149 @@ InlineLexer.prototype.mangle = function(text) {
2575
2645
  return out;
2576
2646
  };
2577
2647
 
2648
+ /**
2649
+ * Renderer
2650
+ */
2651
+
2652
+ function Renderer(options) {
2653
+ this.options = options || {};
2654
+ }
2655
+
2656
+ Renderer.prototype.code = function(code, lang, escaped) {
2657
+ if (this.options.highlight) {
2658
+ var out = this.options.highlight(code, lang);
2659
+ if (out != null && out !== code) {
2660
+ escaped = true;
2661
+ code = out;
2662
+ }
2663
+ }
2664
+
2665
+ if (!lang) {
2666
+ return '<pre><code>'
2667
+ + (escaped ? code : escape(code, true))
2668
+ + '\n</code></pre>';
2669
+ }
2670
+
2671
+ return '<pre><code class="'
2672
+ + this.options.langPrefix
2673
+ + escape(lang, true)
2674
+ + '">'
2675
+ + (escaped ? code : escape(code, true))
2676
+ + '\n</code></pre>\n';
2677
+ };
2678
+
2679
+ Renderer.prototype.blockquote = function(quote) {
2680
+ return '<blockquote>\n' + quote + '</blockquote>\n';
2681
+ };
2682
+
2683
+ Renderer.prototype.html = function(html) {
2684
+ return html;
2685
+ };
2686
+
2687
+ Renderer.prototype.heading = function(text, level, raw) {
2688
+ return '<h'
2689
+ + level
2690
+ + ' id="'
2691
+ + this.options.headerPrefix
2692
+ + raw.toLowerCase().replace(/[^\w]+/g, '-')
2693
+ + '">'
2694
+ + text
2695
+ + '</h'
2696
+ + level
2697
+ + '>\n';
2698
+ };
2699
+
2700
+ Renderer.prototype.hr = function() {
2701
+ return this.options.xhtml ? '<hr/>\n' : '<hr>\n';
2702
+ };
2703
+
2704
+ Renderer.prototype.list = function(body, ordered) {
2705
+ var type = ordered ? 'ol' : 'ul';
2706
+ return '<' + type + '>\n' + body + '</' + type + '>\n';
2707
+ };
2708
+
2709
+ Renderer.prototype.listitem = function(text) {
2710
+ return '<li>' + text + '</li>\n';
2711
+ };
2712
+
2713
+ Renderer.prototype.paragraph = function(text) {
2714
+ return '<p>' + text + '</p>\n';
2715
+ };
2716
+
2717
+ Renderer.prototype.table = function(header, body) {
2718
+ return '<table>\n'
2719
+ + '<thead>\n'
2720
+ + header
2721
+ + '</thead>\n'
2722
+ + '<tbody>\n'
2723
+ + body
2724
+ + '</tbody>\n'
2725
+ + '</table>\n';
2726
+ };
2727
+
2728
+ Renderer.prototype.tablerow = function(content) {
2729
+ return '<tr>\n' + content + '</tr>\n';
2730
+ };
2731
+
2732
+ Renderer.prototype.tablecell = function(content, flags) {
2733
+ var type = flags.header ? 'th' : 'td';
2734
+ var tag = flags.align
2735
+ ? '<' + type + ' style="text-align:' + flags.align + '">'
2736
+ : '<' + type + '>';
2737
+ return tag + content + '</' + type + '>\n';
2738
+ };
2739
+
2740
+ // span level renderer
2741
+ Renderer.prototype.strong = function(text) {
2742
+ return '<strong>' + text + '</strong>';
2743
+ };
2744
+
2745
+ Renderer.prototype.em = function(text) {
2746
+ return '<em>' + text + '</em>';
2747
+ };
2748
+
2749
+ Renderer.prototype.codespan = function(text) {
2750
+ return '<code>' + text + '</code>';
2751
+ };
2752
+
2753
+ Renderer.prototype.br = function() {
2754
+ return this.options.xhtml ? '<br/>' : '<br>';
2755
+ };
2756
+
2757
+ Renderer.prototype.del = function(text) {
2758
+ return '<del>' + text + '</del>';
2759
+ };
2760
+
2761
+ Renderer.prototype.link = function(href, title, text) {
2762
+ if (this.options.sanitize) {
2763
+ try {
2764
+ var prot = decodeURIComponent(unescape(href))
2765
+ .replace(/[^\w:]/g, '')
2766
+ .toLowerCase();
2767
+ } catch (e) {
2768
+ return '';
2769
+ }
2770
+ if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) {
2771
+ return '';
2772
+ }
2773
+ }
2774
+ var out = '<a href="' + href + '"';
2775
+ if (title) {
2776
+ out += ' title="' + title + '"';
2777
+ }
2778
+ out += '>' + text + '</a>';
2779
+ return out;
2780
+ };
2781
+
2782
+ Renderer.prototype.image = function(href, title, text) {
2783
+ var out = '<img src="' + href + '" alt="' + text + '"';
2784
+ if (title) {
2785
+ out += ' title="' + title + '"';
2786
+ }
2787
+ out += this.options.xhtml ? '/>' : '>';
2788
+ return out;
2789
+ };
2790
+
2578
2791
  /**
2579
2792
  * Parsing & Compiling
2580
2793
  */
@@ -2583,14 +2796,17 @@ function Parser(options) {
2583
2796
  this.tokens = [];
2584
2797
  this.token = null;
2585
2798
  this.options = options || marked.defaults;
2799
+ this.options.renderer = this.options.renderer || new Renderer;
2800
+ this.renderer = this.options.renderer;
2801
+ this.renderer.options = this.options;
2586
2802
  }
2587
2803
 
2588
2804
  /**
2589
2805
  * Static Parse Method
2590
2806
  */
2591
2807
 
2592
- Parser.parse = function(src, options) {
2593
- var parser = new Parser(options);
2808
+ Parser.parse = function(src, options, renderer) {
2809
+ var parser = new Parser(options, renderer);
2594
2810
  return parser.parse(src);
2595
2811
  };
2596
2812
 
@@ -2599,7 +2815,7 @@ Parser.parse = function(src, options) {
2599
2815
  */
2600
2816
 
2601
2817
  Parser.prototype.parse = function(src) {
2602
- this.inline = new InlineLexer(src.links, this.options);
2818
+ this.inline = new InlineLexer(src.links, this.options, this.renderer);
2603
2819
  this.tokens = src.reverse();
2604
2820
 
2605
2821
  var out = '';
@@ -2623,7 +2839,7 @@ Parser.prototype.next = function() {
2623
2839
  */
2624
2840
 
2625
2841
  Parser.prototype.peek = function() {
2626
- return this.tokens[this.tokens.length-1] || 0;
2842
+ return this.tokens[this.tokens.length - 1] || 0;
2627
2843
  };
2628
2844
 
2629
2845
  /**
@@ -2650,76 +2866,53 @@ Parser.prototype.tok = function() {
2650
2866
  return '';
2651
2867
  }
2652
2868
  case 'hr': {
2653
- return '<hr>\n';
2869
+ return this.renderer.hr();
2654
2870
  }
2655
2871
  case 'heading': {
2656
- return '<h'
2657
- + this.token.depth
2658
- + '>'
2659
- + this.inline.output(this.token.text)
2660
- + '</h'
2661
- + this.token.depth
2662
- + '>\n';
2872
+ return this.renderer.heading(
2873
+ this.inline.output(this.token.text),
2874
+ this.token.depth,
2875
+ this.token.text);
2663
2876
  }
2664
2877
  case 'code': {
2665
- if (this.options.highlight) {
2666
- var code = this.options.highlight(this.token.text, this.token.lang);
2667
- if (code != null && code !== this.token.text) {
2668
- this.token.escaped = true;
2669
- this.token.text = code;
2670
- }
2671
- }
2672
-
2673
- if (!this.token.escaped) {
2674
- this.token.text = escape(this.token.text, true);
2675
- }
2676
-
2677
- return '<pre><code'
2678
- + (this.token.lang
2679
- ? ' class="lang-'
2680
- + this.token.lang
2681
- + '"'
2682
- : '')
2683
- + '>'
2684
- + this.token.text
2685
- + '</code></pre>\n';
2878
+ return this.renderer.code(this.token.text,
2879
+ this.token.lang,
2880
+ this.token.escaped);
2686
2881
  }
2687
2882
  case 'table': {
2688
- var body = ''
2689
- , heading
2883
+ var header = ''
2884
+ , body = ''
2690
2885
  , i
2691
2886
  , row
2692
2887
  , cell
2888
+ , flags
2693
2889
  , j;
2694
2890
 
2695
2891
  // header
2696
- body += '<thead>\n<tr>\n';
2892
+ cell = '';
2697
2893
  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';
2894
+ flags = { header: true, align: this.token.align[i] };
2895
+ cell += this.renderer.tablecell(
2896
+ this.inline.output(this.token.header[i]),
2897
+ { header: true, align: this.token.align[i] }
2898
+ );
2702
2899
  }
2703
- body += '</tr>\n</thead>\n';
2900
+ header += this.renderer.tablerow(cell);
2704
2901
 
2705
- // body
2706
- body += '<tbody>\n'
2707
2902
  for (i = 0; i < this.token.cells.length; i++) {
2708
2903
  row = this.token.cells[i];
2709
- body += '<tr>\n';
2904
+
2905
+ cell = '';
2710
2906
  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';
2907
+ cell += this.renderer.tablecell(
2908
+ this.inline.output(row[j]),
2909
+ { header: false, align: this.token.align[j] }
2910
+ );
2715
2911
  }
2716
- body += '</tr>\n';
2717
- }
2718
- body += '</tbody>\n';
2719
2912
 
2720
- return '<table>\n'
2721
- + body
2722
- + '</table>\n';
2913
+ body += this.renderer.tablerow(cell);
2914
+ }
2915
+ return this.renderer.table(header, body);
2723
2916
  }
2724
2917
  case 'blockquote_start': {
2725
2918
  var body = '';
@@ -2728,25 +2921,17 @@ Parser.prototype.tok = function() {
2728
2921
  body += this.tok();
2729
2922
  }
2730
2923
 
2731
- return '<blockquote>\n'
2732
- + body
2733
- + '</blockquote>\n';
2924
+ return this.renderer.blockquote(body);
2734
2925
  }
2735
2926
  case 'list_start': {
2736
- var type = this.token.ordered ? 'ol' : 'ul'
2737
- , body = '';
2927
+ var body = ''
2928
+ , ordered = this.token.ordered;
2738
2929
 
2739
2930
  while (this.next().type !== 'list_end') {
2740
2931
  body += this.tok();
2741
2932
  }
2742
2933
 
2743
- return '<'
2744
- + type
2745
- + '>\n'
2746
- + body
2747
- + '</'
2748
- + type
2749
- + '>\n';
2934
+ return this.renderer.list(body, ordered);
2750
2935
  }
2751
2936
  case 'list_item_start': {
2752
2937
  var body = '';
@@ -2757,9 +2942,7 @@ Parser.prototype.tok = function() {
2757
2942
  : this.tok();
2758
2943
  }
2759
2944
 
2760
- return '<li>'
2761
- + body
2762
- + '</li>\n';
2945
+ return this.renderer.listitem(body);
2763
2946
  }
2764
2947
  case 'loose_item_start': {
2765
2948
  var body = '';
@@ -2768,24 +2951,19 @@ Parser.prototype.tok = function() {
2768
2951
  body += this.tok();
2769
2952
  }
2770
2953
 
2771
- return '<li>'
2772
- + body
2773
- + '</li>\n';
2954
+ return this.renderer.listitem(body);
2774
2955
  }
2775
2956
  case 'html': {
2776
- return !this.token.pre && !this.options.pedantic
2957
+ var html = !this.token.pre && !this.options.pedantic
2777
2958
  ? this.inline.output(this.token.text)
2778
2959
  : this.token.text;
2960
+ return this.renderer.html(html);
2779
2961
  }
2780
2962
  case 'paragraph': {
2781
- return '<p>'
2782
- + this.inline.output(this.token.text)
2783
- + '</p>\n';
2963
+ return this.renderer.paragraph(this.inline.output(this.token.text));
2784
2964
  }
2785
2965
  case 'text': {
2786
- return '<p>'
2787
- + this.parseText()
2788
- + '</p>\n';
2966
+ return this.renderer.paragraph(this.parseText());
2789
2967
  }
2790
2968
  }
2791
2969
  };
@@ -2803,6 +2981,19 @@ function escape(html, encode) {
2803
2981
  .replace(/'/g, '&#39;');
2804
2982
  }
2805
2983
 
2984
+ function unescape(html) {
2985
+ return html.replace(/&([#\w]+);/g, function(_, n) {
2986
+ n = n.toLowerCase();
2987
+ if (n === 'colon') return ':';
2988
+ if (n.charAt(0) === '#') {
2989
+ return n.charAt(1) === 'x'
2990
+ ? String.fromCharCode(parseInt(n.substring(2), 16))
2991
+ : String.fromCharCode(+n.substring(1));
2992
+ }
2993
+ return '';
2994
+ });
2995
+ }
2996
+
2806
2997
  function replace(regex, opt) {
2807
2998
  regex = regex.source;
2808
2999
  opt = opt || '';
@@ -2835,17 +3026,90 @@ function merge(obj) {
2835
3026
  return obj;
2836
3027
  }
2837
3028
 
3029
+
2838
3030
  /**
2839
3031
  * Marked
2840
3032
  */
2841
3033
 
2842
- function marked(src, opt) {
3034
+ function marked(src, opt, callback) {
3035
+ if (callback || typeof opt === 'function') {
3036
+ if (!callback) {
3037
+ callback = opt;
3038
+ opt = null;
3039
+ }
3040
+
3041
+ opt = merge({}, marked.defaults, opt || {});
3042
+
3043
+ var highlight = opt.highlight
3044
+ , tokens
3045
+ , pending
3046
+ , i = 0;
3047
+
3048
+ try {
3049
+ tokens = Lexer.lex(src, opt)
3050
+ } catch (e) {
3051
+ return callback(e);
3052
+ }
3053
+
3054
+ pending = tokens.length;
3055
+
3056
+ var done = function(err) {
3057
+ if (err) {
3058
+ opt.highlight = highlight;
3059
+ return callback(err);
3060
+ }
3061
+
3062
+ var out;
3063
+
3064
+ try {
3065
+ out = Parser.parse(tokens, opt);
3066
+ } catch (e) {
3067
+ err = e;
3068
+ }
3069
+
3070
+ opt.highlight = highlight;
3071
+
3072
+ return err
3073
+ ? callback(err)
3074
+ : callback(null, out);
3075
+ };
3076
+
3077
+ if (!highlight || highlight.length < 3) {
3078
+ return done();
3079
+ }
3080
+
3081
+ delete opt.highlight;
3082
+
3083
+ if (!pending) return done();
3084
+
3085
+ for (; i < tokens.length; i++) {
3086
+ (function(token) {
3087
+ if (token.type !== 'code') {
3088
+ return --pending || done();
3089
+ }
3090
+ return highlight(token.text, token.lang, function(err, code) {
3091
+ if (err) return done(err);
3092
+ if (code == null || code === token.text) {
3093
+ return --pending || done();
3094
+ }
3095
+ token.text = code;
3096
+ token.escaped = true;
3097
+ --pending || done();
3098
+ });
3099
+ })(tokens[i]);
3100
+ }
3101
+
3102
+ return;
3103
+ }
2843
3104
  try {
3105
+ if (opt) opt = merge({}, marked.defaults, opt);
2844
3106
  return Parser.parse(Lexer.lex(src, opt), opt);
2845
3107
  } catch (e) {
2846
3108
  e.message += '\nPlease report this to https://github.com/chjj/marked.';
2847
3109
  if ((opt || marked.defaults).silent) {
2848
- return 'An error occured:\n' + e.message;
3110
+ return '<p>An error occured:</p><pre>'
3111
+ + escape(e.message + '', true)
3112
+ + '</pre>';
2849
3113
  }
2850
3114
  throw e;
2851
3115
  }
@@ -2857,7 +3121,7 @@ function marked(src, opt) {
2857
3121
 
2858
3122
  marked.options =
2859
3123
  marked.setOptions = function(opt) {
2860
- marked.defaults = opt;
3124
+ merge(marked.defaults, opt);
2861
3125
  return marked;
2862
3126
  };
2863
3127
 
@@ -2867,8 +3131,14 @@ marked.defaults = {
2867
3131
  breaks: false,
2868
3132
  pedantic: false,
2869
3133
  sanitize: false,
3134
+ smartLists: false,
2870
3135
  silent: false,
2871
- highlight: null
3136
+ highlight: null,
3137
+ langPrefix: 'lang-',
3138
+ smartypants: false,
3139
+ headerPrefix: '',
3140
+ renderer: new Renderer,
3141
+ xhtml: false
2872
3142
  };
2873
3143
 
2874
3144
  /**
@@ -2878,6 +3148,8 @@ marked.defaults = {
2878
3148
  marked.Parser = Parser;
2879
3149
  marked.parser = Parser.parse;
2880
3150
 
3151
+ marked.Renderer = Renderer;
3152
+
2881
3153
  marked.Lexer = Lexer;
2882
3154
  marked.lexer = Lexer.lex;
2883
3155
 
@@ -2886,7 +3158,7 @@ marked.inlineLexer = InlineLexer.output;
2886
3158
 
2887
3159
  marked.parse = marked;
2888
3160
 
2889
- if (typeof module !== 'undefined') {
3161
+ if (typeof module !== 'undefined' && typeof exports === 'object') {
2890
3162
  module.exports = marked;
2891
3163
  } else if (typeof define === 'function' && define.amd) {
2892
3164
  define(function() { return marked; });