epic-editor-rails 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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; });