Forráskód Böngészése

feat(eventDispatcher): add an event dispatcher to converter

Estevão Soares dos Santos 10 éve
szülő
commit
2734326e19

+ 156 - 54
dist/showdown.js

@@ -1,4 +1,4 @@
-;/*! showdown 02-08-2015 */
+;/*! showdown 03-08-2015 */
 (function(){
 /**
  * Created by Tivie on 13-07-2015.
@@ -333,27 +333,57 @@ function validate(extension, name) {
       type = ext.type = 'output';
     }
 
-    if (type !== 'lang' && type !== 'output') {
+    if (type !== 'lang' && type !== 'output' && type !== 'listener') {
       ret.valid = false;
-      ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang" or "output"';
+      ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"';
       return ret;
     }
 
+    if (type === 'listener') {
+      if (showdown.helper.isUndefined(ext.listeners)) {
+        ret.valid = false;
+        ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"';
+        return ret;
+      }
+    } else {
+      if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
+        ret.valid = false;
+        ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method';
+        return ret;
+      }
+    }
+
+    if (ext.listeners) {
+      if (typeof ext.listeners !== 'object') {
+        ret.valid = false;
+        ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given';
+        return ret;
+      }
+      for (var ln in ext.listeners) {
+        if (ext.listeners.hasOwnProperty(ln)) {
+          if (typeof ext.listeners[ln] !== 'function') {
+            ret.valid = false;
+            ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln +
+              ' must be a function but ' + typeof ext.listeners[ln] + ' given';
+            return ret;
+          }
+        }
+      }
+    }
+
     if (ext.filter) {
       if (typeof ext.filter !== 'function') {
         ret.valid = false;
         ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given';
         return ret;
       }
-
     } else if (ext.regex) {
       if (showdown.helper.isString(ext.regex)) {
         ext.regex = new RegExp(ext.regex, 'g');
       }
       if (!ext.regex instanceof RegExp) {
         ret.valid = false;
-        ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' +
-          typeof ext.regex + ' given';
+        ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given';
         return ret;
       }
       if (showdown.helper.isUndefined(ext.replace)) {
@@ -361,17 +391,6 @@ function validate(extension, name) {
         ret.error = baseMsg + '"regex" extensions must implement a replace string or function';
         return ret;
       }
-
-    } else {
-      ret.valid = false;
-      ret.error = baseMsg + 'extensions must define either a "regex" property or a "filter" method';
-      return ret;
-    }
-
-    if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
-      ret.valid = false;
-      ret.error = baseMsg + 'output extensions must define a filter property';
-      return ret;
     }
   }
   return ret;
@@ -557,18 +576,8 @@ showdown.Converter = function (converterOptions) {
        */
       outputModifiers = [],
 
-      /**
-       * The parser Order
-       * @private
-       * @type {string[]}
-       */
-      parserOrder = [
-        'githubCodeBlocks',
-        'hashHTMLBlocks',
-        'stripLinkDefinitions',
-        'blockGamut',
-        'unescapeSpecialChars'
-      ];
+      listeners = {
+      };
 
   _constructor();
 
@@ -647,6 +656,7 @@ showdown.Converter = function (converterOptions) {
 
     for (var i = 0; i < ext.length; ++i) {
       switch (ext[i].type) {
+
         case 'lang':
           langExtensions.push(ext[i]);
           break;
@@ -654,12 +664,16 @@ showdown.Converter = function (converterOptions) {
         case 'output':
           outputModifiers.push(ext[i]);
           break;
-
-        default:
-          // should never reach here
-          throw Error('Extension loader error: Type unrecognized!!!');
+      }
+      if (ext[i].hasOwnProperty(listeners)) {
+        for (var ln in ext[i].listeners) {
+          if (ext[i].listeners.hasOwnProperty(ln)) {
+            listen(ln, ext[i].listeners[ln]);
+          }
+        }
       }
     }
+
   }
 
   /**
@@ -694,6 +708,57 @@ showdown.Converter = function (converterOptions) {
     }
   }
 
+  /**
+   * Listen to an event
+   * @param {string} name
+   * @param {function} callback
+   */
+  function listen(name, callback) {
+    if (!showdown.helper.isString(name)) {
+      throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given');
+    }
+
+    if (typeof callback !== 'function') {
+      throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given');
+    }
+
+    if (!listeners.hasOwnProperty(name)) {
+      listeners[name] = [];
+    }
+    listeners[name].push(callback);
+  }
+
+  /**
+   * Dispatch an event
+   * @private
+   * @param {string} evtName Event name
+   * @param {string} text Text
+   * @param {{}} options Converter Options
+   * @returns {string}
+   */
+  this._dispatch = function dispatch (evtName, text, options) {
+    if (listeners.hasOwnProperty(evtName)) {
+      for (var ei = 0; ei < listeners[evtName].length; ++ei) {
+        var nText = listeners[evtName][ei](evtName, text, this, options);
+        if (nText && typeof nText !== 'undefined') {
+          text = nText;
+        }
+      }
+    }
+    return text;
+  };
+
+  /**
+   * Listen to an event
+   * @param {string} name
+   * @param {function} callback
+   * @returns {showdown.Converter}
+   */
+  this.listen = function (name, callback) {
+    listen(name, callback);
+    return this;
+  };
+
   /**
    * Converts a markdown string into HTML
    * @param {string} text
@@ -746,11 +811,12 @@ showdown.Converter = function (converterOptions) {
       text = showdown.subParser('runExtension')(ext, text, options, globals);
     });
 
-    // Run all registered parsers
-    for (var i = 0; i < parserOrder.length; ++i) {
-      var name = parserOrder[i];
-      text = parsers[name](text, options, globals);
-    }
+    // run the sub parsers
+    text = showdown.subParser('githubCodeBlocks')(text, options, globals);
+    text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
+    text = showdown.subParser('stripLinkDefinitions')(text, options, globals);
+    text = showdown.subParser('blockGamut')(text, options, globals);
+    text = showdown.subParser('unescapeSpecialChars')(text, options, globals);
 
     // attacklab: Restore dollar signs
     text = text.replace(/~D/g, '$$');
@@ -865,9 +931,11 @@ showdown.Converter = function (converterOptions) {
 /**
  * Turn Markdown link shortcuts into XHTML <a> tags.
  */
-showdown.subParser('anchors', function (text, config, globals) {
+showdown.subParser('anchors', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('anchors.before', text, options);
+
   var writeAnchorTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
     if (showdown.helper.isUndefined(m7)) {
       m7 = '';
@@ -937,7 +1005,7 @@ showdown.subParser('anchors', function (text, config, globals) {
    )()()()()					// pad remaining backreferences
    /g,_DoAnchors_callback);
    */
-  text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);
+  text = text.replace(/(\[((?:\[[^\]]*]|[^\[\]])*)][ ]?(?:\n[ ]*)?\[(.*?)])()()()()/g, writeAnchorTag);
 
   //
   // Next, inline-style links: [link text](url "optional title")
@@ -970,7 +1038,7 @@ showdown.subParser('anchors', function (text, config, globals) {
    )
    /g,writeAnchorTag);
    */
-  text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
+  text = text.replace(/(\[((?:\[[^\]]*]|[^\[\]])*)]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
                       writeAnchorTag);
 
   //
@@ -988,16 +1056,16 @@ showdown.subParser('anchors', function (text, config, globals) {
    )()()()()()      // pad rest of backreferences
    /g, writeAnchorTag);
    */
-  text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
+  text = text.replace(/(\[([^\[\]]+)])()()()()()/g, writeAnchorTag);
 
+  text = globals.converter._dispatch('anchors.after', text, options);
   return text;
-
 });
 
-showdown.subParser('autoLinks', function (text, options) {
+showdown.subParser('autoLinks', function (text, options, globals) {
   'use strict';
 
-  //simpleURLRegex  = /\b(((https?|ftp|dict):\/\/|www\.)[-.+~:?#@!$&'()*,;=[\]\w]+)\b/gi,
+  text = globals.converter._dispatch('autoLinks.before', text, options);
 
   var simpleURLRegex  = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+)(?=\s|$)(?!["<>])/gi,
       delimUrlRegex   = /<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)>/gi,
@@ -1019,6 +1087,8 @@ showdown.subParser('autoLinks', function (text, options) {
     return showdown.subParser('encodeEmailAddress')(unescapedStr);
   }
 
+  text = globals.converter._dispatch('autoLinks.after', text, options);
+
   return text;
 });
 
@@ -1029,6 +1099,8 @@ showdown.subParser('autoLinks', function (text, options) {
 showdown.subParser('blockGamut', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('blockGamut.before', text, options);
+
   text = showdown.subParser('headers')(text, options, globals);
 
   // Do Horizontal Rules:
@@ -1049,13 +1121,15 @@ showdown.subParser('blockGamut', function (text, options, globals) {
   text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
   text = showdown.subParser('paragraphs')(text, options, globals);
 
-  return text;
+  text = globals.converter._dispatch('blockGamut.after', text, options);
 
+  return text;
 });
 
 showdown.subParser('blockQuotes', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('blockQuotes.before', text, options);
   /*
    text = text.replace(/
    (								// Wrap whole match in $1
@@ -1094,6 +1168,8 @@ showdown.subParser('blockQuotes', function (text, options, globals) {
 
     return showdown.subParser('hashBlock')('<blockquote>\n' + bq + '\n</blockquote>', options, globals);
   });
+
+  text = globals.converter._dispatch('blockQuotes.after', text, options);
   return text;
 });
 
@@ -1103,6 +1179,7 @@ showdown.subParser('blockQuotes', function (text, options, globals) {
 showdown.subParser('codeBlocks', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('codeBlocks.before', text, options);
   /*
    text = text.replace(text,
    /(?:\n\n|^)
@@ -1143,6 +1220,7 @@ showdown.subParser('codeBlocks', function (text, options, globals) {
   // attacklab: strip sentinel
   text = text.replace(/~0/, '');
 
+  text = globals.converter._dispatch('codeBlocks.after', text, options);
   return text;
 });
 
@@ -1171,9 +1249,10 @@ showdown.subParser('codeBlocks', function (text, options, globals) {
  *
  *         ... type <code>`bar`</code> ...
  */
-showdown.subParser('codeSpans', function (text) {
+showdown.subParser('codeSpans', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('codeSpans.before', text, options);
   //special case -> literal html code tag
   text = text.replace(/(<code[^><]*?>)([^]*?)<\/code>/g, function (wholeMatch, tag, c) {
     c = c.replace(/^([ \t]*)/g, '');	// leading whitespace
@@ -1204,6 +1283,7 @@ showdown.subParser('codeSpans', function (text) {
     }
   );
 
+  text = globals.converter._dispatch('codeSpans.after', text, options);
   return text;
 });
 
@@ -1393,6 +1473,8 @@ showdown.subParser('githubCodeBlocks', function (text, options, globals) {
     return text;
   }
 
+  text = globals.converter._dispatch('githubCodeBlocks.before', text, options);
+
   text += '~0';
 
   text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g, function (wholeMatch, language, codeblock) {
@@ -1411,8 +1493,9 @@ showdown.subParser('githubCodeBlocks', function (text, options, globals) {
   // attacklab: strip sentinel
   text = text.replace(/~0/, '');
 
-  return text;
+  text = globals.converter._dispatch('githubCodeBlocks.after', text, options);
 
+  return text;
 });
 
 showdown.subParser('hashBlock', function (text, options, globals) {
@@ -1578,6 +1661,8 @@ showdown.subParser('hashHTMLBlocks', function (text, options, globals) {
 showdown.subParser('headers', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('headers.before', text, options);
+
   var prefixHeader = options.prefixHeaderId,
       headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart),
 
@@ -1645,6 +1730,7 @@ showdown.subParser('headers', function (text, options, globals) {
     return title;
   }
 
+  text = globals.converter._dispatch('headers.after', text, options);
   return text;
 });
 
@@ -1654,6 +1740,8 @@ showdown.subParser('headers', function (text, options, globals) {
 showdown.subParser('images', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('images.before', text, options);
+
   var inlineRegExp    = /!\[(.*?)]\s?\([ \t]*()<?(\S+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(['"])(.*?)\6[ \t]*)?\)/g,
       referenceRegExp = /!\[(.*?)][ ]?(?:\n[ ]*)?\[(.*?)]()()()()()/g;
 
@@ -1720,12 +1808,15 @@ showdown.subParser('images', function (text, options, globals) {
   // Next, handle inline images:  ![alt text](url =<width>x<height> "optional title")
   text = text.replace(inlineRegExp, writeImageTag);
 
+  text = globals.converter._dispatch('images.after', text, options);
   return text;
 });
 
-showdown.subParser('italicsAndBold', function (text, options) {
+showdown.subParser('italicsAndBold', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('italicsAndBold.before', text, options);
+
   if (options.literalMidWordUnderscores) {
     //underscores
     // Since we are consuming a \s character, we need to add it
@@ -1740,6 +1831,8 @@ showdown.subParser('italicsAndBold', function (text, options) {
     text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, '<strong>$2</strong>');
     text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, '<em>$2</em>');
   }
+
+  text = globals.converter._dispatch('italicsAndBold.after', text, options);
   return text;
 });
 
@@ -1749,6 +1842,7 @@ showdown.subParser('italicsAndBold', function (text, options) {
 showdown.subParser('lists', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('lists.before', text, options);
   /**
    * Process the contents of a single ordered or unordered list, splitting it
    * into individual list items.
@@ -1901,6 +1995,7 @@ showdown.subParser('lists', function (text, options, globals) {
   // attacklab: strip sentinel
   text = text.replace(/~0/, '');
 
+  text = globals.converter._dispatch('lists.after', text, options);
   return text;
 });
 
@@ -1926,6 +2021,7 @@ showdown.subParser('outdent', function (text) {
 showdown.subParser('paragraphs', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('paragraphs.before', text, options);
   // Strip leading and trailing lines:
   text = text.replace(/^\n+/g, '');
   text = text.replace(/\n+$/g, '');
@@ -1959,6 +2055,7 @@ showdown.subParser('paragraphs', function (text, options, globals) {
     }
   }
 
+  text = globals.converter._dispatch('paragraphs.after', text, options);
   return grafsOut.join('\n\n');
 });
 
@@ -1990,6 +2087,7 @@ showdown.subParser('runExtension', function (ext, text, options, globals) {
 showdown.subParser('spanGamut', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('spanGamut.before', text, options);
   text = showdown.subParser('codeSpans')(text, options, globals);
   text = showdown.subParser('escapeSpecialCharsWithinTagAttributes')(text, options, globals);
   text = showdown.subParser('encodeBackslashEscapes')(text, options, globals);
@@ -2010,15 +2108,17 @@ showdown.subParser('spanGamut', function (text, options, globals) {
   // Do hard breaks:
   text = text.replace(/  +\n/g, ' <br />\n');
 
+  text = globals.converter._dispatch('spanGamut.after', text, options);
   return text;
-
 });
 
-showdown.subParser('strikethrough', function (text, options) {
+showdown.subParser('strikethrough', function (text, options, globals) {
   'use strict';
 
   if (options.strikethrough) {
+    text = globals.converter._dispatch('strikethrough.before', text, options);
     text = text.replace(/(?:~T){2}([^~]+)(?:~T){2}/g, '<del>$1</del>');
+    text = globals.converter._dispatch('strikethrough.after', text, options);
   }
 
   return text;
@@ -2254,11 +2354,13 @@ showdown.subParser('tables', function (text, options, globals) {
   };
 
   if (options.tables) {
+    text = globals.converter._dispatch('tables.before', text, options);
     var tableParser = table();
-    return tableParser.parse(text);
-  } else {
-    return text;
+    text = tableParser.parse(text);
+    text = globals.converter._dispatch('tables.after', text, options);
   }
+
+  return text;
 });
 
 /**

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
dist/showdown.js.map


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
dist/showdown.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
dist/showdown.min.js.map


+ 68 - 21
src/converter.js

@@ -38,18 +38,8 @@ showdown.Converter = function (converterOptions) {
        */
       outputModifiers = [],
 
-      /**
-       * The parser Order
-       * @private
-       * @type {string[]}
-       */
-      parserOrder = [
-        'githubCodeBlocks',
-        'hashHTMLBlocks',
-        'stripLinkDefinitions',
-        'blockGamut',
-        'unescapeSpecialChars'
-      ];
+      listeners = {
+      };
 
   _constructor();
 
@@ -128,6 +118,7 @@ showdown.Converter = function (converterOptions) {
 
     for (var i = 0; i < ext.length; ++i) {
       switch (ext[i].type) {
+
         case 'lang':
           langExtensions.push(ext[i]);
           break;
@@ -135,12 +126,16 @@ showdown.Converter = function (converterOptions) {
         case 'output':
           outputModifiers.push(ext[i]);
           break;
-
-        default:
-          // should never reach here
-          throw Error('Extension loader error: Type unrecognized!!!');
+      }
+      if (ext[i].hasOwnProperty(listeners)) {
+        for (var ln in ext[i].listeners) {
+          if (ext[i].listeners.hasOwnProperty(ln)) {
+            listen(ln, ext[i].listeners[ln]);
+          }
+        }
       }
     }
+
   }
 
   /**
@@ -175,6 +170,57 @@ showdown.Converter = function (converterOptions) {
     }
   }
 
+  /**
+   * Listen to an event
+   * @param {string} name
+   * @param {function} callback
+   */
+  function listen(name, callback) {
+    if (!showdown.helper.isString(name)) {
+      throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given');
+    }
+
+    if (typeof callback !== 'function') {
+      throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given');
+    }
+
+    if (!listeners.hasOwnProperty(name)) {
+      listeners[name] = [];
+    }
+    listeners[name].push(callback);
+  }
+
+  /**
+   * Dispatch an event
+   * @private
+   * @param {string} evtName Event name
+   * @param {string} text Text
+   * @param {{}} options Converter Options
+   * @returns {string}
+   */
+  this._dispatch = function dispatch (evtName, text, options) {
+    if (listeners.hasOwnProperty(evtName)) {
+      for (var ei = 0; ei < listeners[evtName].length; ++ei) {
+        var nText = listeners[evtName][ei](evtName, text, this, options);
+        if (nText && typeof nText !== 'undefined') {
+          text = nText;
+        }
+      }
+    }
+    return text;
+  };
+
+  /**
+   * Listen to an event
+   * @param {string} name
+   * @param {function} callback
+   * @returns {showdown.Converter}
+   */
+  this.listen = function (name, callback) {
+    listen(name, callback);
+    return this;
+  };
+
   /**
    * Converts a markdown string into HTML
    * @param {string} text
@@ -227,11 +273,12 @@ showdown.Converter = function (converterOptions) {
       text = showdown.subParser('runExtension')(ext, text, options, globals);
     });
 
-    // Run all registered parsers
-    for (var i = 0; i < parserOrder.length; ++i) {
-      var name = parserOrder[i];
-      text = parsers[name](text, options, globals);
-    }
+    // run the sub parsers
+    text = showdown.subParser('githubCodeBlocks')(text, options, globals);
+    text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
+    text = showdown.subParser('stripLinkDefinitions')(text, options, globals);
+    text = showdown.subParser('blockGamut')(text, options, globals);
+    text = showdown.subParser('unescapeSpecialChars')(text, options, globals);
 
     // attacklab: Restore dollar signs
     text = text.replace(/~D/g, '$$');

+ 35 - 16
src/showdown.js

@@ -245,27 +245,57 @@ function validate(extension, name) {
       type = ext.type = 'output';
     }
 
-    if (type !== 'lang' && type !== 'output') {
+    if (type !== 'lang' && type !== 'output' && type !== 'listener') {
       ret.valid = false;
-      ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang" or "output"';
+      ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"';
       return ret;
     }
 
+    if (type === 'listener') {
+      if (showdown.helper.isUndefined(ext.listeners)) {
+        ret.valid = false;
+        ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"';
+        return ret;
+      }
+    } else {
+      if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
+        ret.valid = false;
+        ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method';
+        return ret;
+      }
+    }
+
+    if (ext.listeners) {
+      if (typeof ext.listeners !== 'object') {
+        ret.valid = false;
+        ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given';
+        return ret;
+      }
+      for (var ln in ext.listeners) {
+        if (ext.listeners.hasOwnProperty(ln)) {
+          if (typeof ext.listeners[ln] !== 'function') {
+            ret.valid = false;
+            ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln +
+              ' must be a function but ' + typeof ext.listeners[ln] + ' given';
+            return ret;
+          }
+        }
+      }
+    }
+
     if (ext.filter) {
       if (typeof ext.filter !== 'function') {
         ret.valid = false;
         ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given';
         return ret;
       }
-
     } else if (ext.regex) {
       if (showdown.helper.isString(ext.regex)) {
         ext.regex = new RegExp(ext.regex, 'g');
       }
       if (!ext.regex instanceof RegExp) {
         ret.valid = false;
-        ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' +
-          typeof ext.regex + ' given';
+        ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given';
         return ret;
       }
       if (showdown.helper.isUndefined(ext.replace)) {
@@ -273,17 +303,6 @@ function validate(extension, name) {
         ret.error = baseMsg + '"regex" extensions must implement a replace string or function';
         return ret;
       }
-
-    } else {
-      ret.valid = false;
-      ret.error = baseMsg + 'extensions must define either a "regex" property or a "filter" method';
-      return ret;
-    }
-
-    if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
-      ret.valid = false;
-      ret.error = baseMsg + 'output extensions must define a filter property';
-      return ret;
     }
   }
   return ret;

+ 7 - 5
src/subParsers/anchors.js

@@ -1,9 +1,11 @@
 /**
  * Turn Markdown link shortcuts into XHTML <a> tags.
  */
-showdown.subParser('anchors', function (text, config, globals) {
+showdown.subParser('anchors', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('anchors.before', text, options);
+
   var writeAnchorTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
     if (showdown.helper.isUndefined(m7)) {
       m7 = '';
@@ -73,7 +75,7 @@ showdown.subParser('anchors', function (text, config, globals) {
    )()()()()					// pad remaining backreferences
    /g,_DoAnchors_callback);
    */
-  text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);
+  text = text.replace(/(\[((?:\[[^\]]*]|[^\[\]])*)][ ]?(?:\n[ ]*)?\[(.*?)])()()()()/g, writeAnchorTag);
 
   //
   // Next, inline-style links: [link text](url "optional title")
@@ -106,7 +108,7 @@ showdown.subParser('anchors', function (text, config, globals) {
    )
    /g,writeAnchorTag);
    */
-  text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
+  text = text.replace(/(\[((?:\[[^\]]*]|[^\[\]])*)]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
                       writeAnchorTag);
 
   //
@@ -124,8 +126,8 @@ showdown.subParser('anchors', function (text, config, globals) {
    )()()()()()      // pad rest of backreferences
    /g, writeAnchorTag);
    */
-  text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
+  text = text.replace(/(\[([^\[\]]+)])()()()()()/g, writeAnchorTag);
 
+  text = globals.converter._dispatch('anchors.after', text, options);
   return text;
-
 });

+ 4 - 2
src/subParsers/autoLinks.js

@@ -1,7 +1,7 @@
-showdown.subParser('autoLinks', function (text, options) {
+showdown.subParser('autoLinks', function (text, options, globals) {
   'use strict';
 
-  //simpleURLRegex  = /\b(((https?|ftp|dict):\/\/|www\.)[-.+~:?#@!$&'()*,;=[\]\w]+)\b/gi,
+  text = globals.converter._dispatch('autoLinks.before', text, options);
 
   var simpleURLRegex  = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+)(?=\s|$)(?!["<>])/gi,
       delimUrlRegex   = /<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)>/gi,
@@ -23,5 +23,7 @@ showdown.subParser('autoLinks', function (text, options) {
     return showdown.subParser('encodeEmailAddress')(unescapedStr);
   }
 
+  text = globals.converter._dispatch('autoLinks.after', text, options);
+
   return text;
 });

+ 4 - 1
src/subParsers/blockGamut.js

@@ -5,6 +5,8 @@
 showdown.subParser('blockGamut', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('blockGamut.before', text, options);
+
   text = showdown.subParser('headers')(text, options, globals);
 
   // Do Horizontal Rules:
@@ -25,6 +27,7 @@ showdown.subParser('blockGamut', function (text, options, globals) {
   text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
   text = showdown.subParser('paragraphs')(text, options, globals);
 
-  return text;
+  text = globals.converter._dispatch('blockGamut.after', text, options);
 
+  return text;
 });

+ 3 - 0
src/subParsers/blockQuotes.js

@@ -1,6 +1,7 @@
 showdown.subParser('blockQuotes', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('blockQuotes.before', text, options);
   /*
    text = text.replace(/
    (								// Wrap whole match in $1
@@ -39,5 +40,7 @@ showdown.subParser('blockQuotes', function (text, options, globals) {
 
     return showdown.subParser('hashBlock')('<blockquote>\n' + bq + '\n</blockquote>', options, globals);
   });
+
+  text = globals.converter._dispatch('blockQuotes.after', text, options);
   return text;
 });

+ 2 - 0
src/subParsers/codeBlocks.js

@@ -4,6 +4,7 @@
 showdown.subParser('codeBlocks', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('codeBlocks.before', text, options);
   /*
    text = text.replace(text,
    /(?:\n\n|^)
@@ -44,5 +45,6 @@ showdown.subParser('codeBlocks', function (text, options, globals) {
   // attacklab: strip sentinel
   text = text.replace(/~0/, '');
 
+  text = globals.converter._dispatch('codeBlocks.after', text, options);
   return text;
 });

+ 3 - 1
src/subParsers/codeSpans.js

@@ -23,9 +23,10 @@
  *
  *         ... type <code>`bar`</code> ...
  */
-showdown.subParser('codeSpans', function (text) {
+showdown.subParser('codeSpans', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('codeSpans.before', text, options);
   //special case -> literal html code tag
   text = text.replace(/(<code[^><]*?>)([^]*?)<\/code>/g, function (wholeMatch, tag, c) {
     c = c.replace(/^([ \t]*)/g, '');	// leading whitespace
@@ -56,5 +57,6 @@ showdown.subParser('codeSpans', function (text) {
     }
   );
 
+  text = globals.converter._dispatch('codeSpans.after', text, options);
   return text;
 });

+ 4 - 1
src/subParsers/githubCodeBlocks.js

@@ -16,6 +16,8 @@ showdown.subParser('githubCodeBlocks', function (text, options, globals) {
     return text;
   }
 
+  text = globals.converter._dispatch('githubCodeBlocks.before', text, options);
+
   text += '~0';
 
   text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g, function (wholeMatch, language, codeblock) {
@@ -34,6 +36,7 @@ showdown.subParser('githubCodeBlocks', function (text, options, globals) {
   // attacklab: strip sentinel
   text = text.replace(/~0/, '');
 
-  return text;
+  text = globals.converter._dispatch('githubCodeBlocks.after', text, options);
 
+  return text;
 });

+ 3 - 0
src/subParsers/headers.js

@@ -1,6 +1,8 @@
 showdown.subParser('headers', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('headers.before', text, options);
+
   var prefixHeader = options.prefixHeaderId,
       headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart),
 
@@ -68,5 +70,6 @@ showdown.subParser('headers', function (text, options, globals) {
     return title;
   }
 
+  text = globals.converter._dispatch('headers.after', text, options);
   return text;
 });

+ 3 - 0
src/subParsers/images.js

@@ -4,6 +4,8 @@
 showdown.subParser('images', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('images.before', text, options);
+
   var inlineRegExp    = /!\[(.*?)]\s?\([ \t]*()<?(\S+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(['"])(.*?)\6[ \t]*)?\)/g,
       referenceRegExp = /!\[(.*?)][ ]?(?:\n[ ]*)?\[(.*?)]()()()()()/g;
 
@@ -70,5 +72,6 @@ showdown.subParser('images', function (text, options, globals) {
   // Next, handle inline images:  ![alt text](url =<width>x<height> "optional title")
   text = text.replace(inlineRegExp, writeImageTag);
 
+  text = globals.converter._dispatch('images.after', text, options);
   return text;
 });

+ 5 - 1
src/subParsers/italicsAndBold.js

@@ -1,6 +1,8 @@
-showdown.subParser('italicsAndBold', function (text, options) {
+showdown.subParser('italicsAndBold', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('italicsAndBold.before', text, options);
+
   if (options.literalMidWordUnderscores) {
     //underscores
     // Since we are consuming a \s character, we need to add it
@@ -15,5 +17,7 @@ showdown.subParser('italicsAndBold', function (text, options) {
     text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, '<strong>$2</strong>');
     text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, '<em>$2</em>');
   }
+
+  text = globals.converter._dispatch('italicsAndBold.after', text, options);
   return text;
 });

+ 2 - 0
src/subParsers/lists.js

@@ -4,6 +4,7 @@
 showdown.subParser('lists', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('lists.before', text, options);
   /**
    * Process the contents of a single ordered or unordered list, splitting it
    * into individual list items.
@@ -156,5 +157,6 @@ showdown.subParser('lists', function (text, options, globals) {
   // attacklab: strip sentinel
   text = text.replace(/~0/, '');
 
+  text = globals.converter._dispatch('lists.after', text, options);
   return text;
 });

+ 2 - 0
src/subParsers/paragraphs.js

@@ -4,6 +4,7 @@
 showdown.subParser('paragraphs', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('paragraphs.before', text, options);
   // Strip leading and trailing lines:
   text = text.replace(/^\n+/g, '');
   text = text.replace(/\n+$/g, '');
@@ -37,5 +38,6 @@ showdown.subParser('paragraphs', function (text, options, globals) {
     }
   }
 
+  text = globals.converter._dispatch('paragraphs.after', text, options);
   return grafsOut.join('\n\n');
 });

+ 2 - 1
src/subParsers/spanGamut.js

@@ -5,6 +5,7 @@
 showdown.subParser('spanGamut', function (text, options, globals) {
   'use strict';
 
+  text = globals.converter._dispatch('spanGamut.before', text, options);
   text = showdown.subParser('codeSpans')(text, options, globals);
   text = showdown.subParser('escapeSpecialCharsWithinTagAttributes')(text, options, globals);
   text = showdown.subParser('encodeBackslashEscapes')(text, options, globals);
@@ -25,6 +26,6 @@ showdown.subParser('spanGamut', function (text, options, globals) {
   // Do hard breaks:
   text = text.replace(/  +\n/g, ' <br />\n');
 
+  text = globals.converter._dispatch('spanGamut.after', text, options);
   return text;
-
 });

+ 3 - 1
src/subParsers/strikethrough.js

@@ -1,8 +1,10 @@
-showdown.subParser('strikethrough', function (text, options) {
+showdown.subParser('strikethrough', function (text, options, globals) {
   'use strict';
 
   if (options.strikethrough) {
+    text = globals.converter._dispatch('strikethrough.before', text, options);
     text = text.replace(/(?:~T){2}([^~]+)(?:~T){2}/g, '<del>$1</del>');
+    text = globals.converter._dispatch('strikethrough.after', text, options);
   }
 
   return text;

+ 5 - 3
src/subParsers/tables.js

@@ -154,9 +154,11 @@ showdown.subParser('tables', function (text, options, globals) {
   };
 
   if (options.tables) {
+    text = globals.converter._dispatch('tables.before', text, options);
     var tableParser = table();
-    return tableParser.parse(text);
-  } else {
-    return text;
+    text = tableParser.parse(text);
+    text = globals.converter._dispatch('tables.after', text, options);
   }
+
+  return text;
 });

+ 55 - 12
test/node/showdown.Converter.js

@@ -27,8 +27,13 @@ describe('showdown.Converter', function () {
     });
   });
 
-  describe('setFlavor() github', function () {
-    var converter = new showdown.Converter(),
+  describe('setFlavor method', function () {
+
+    /**
+     * Test setFlavor('github')
+     */
+    describe('github', function () {
+      var converter = new showdown.Converter(),
         ghOpts = {
           omitExtraWLInCodeBlocks:   true,
           prefixHeaderId:            'user-content-',
@@ -41,18 +46,19 @@ describe('showdown.Converter', function () {
           tasklists:                 true
         };
 
-    converter.setFlavor('github');
+      converter.setFlavor('github');
 
-    for (var opt in ghOpts) {
-      if (ghOpts.hasOwnProperty(opt)) {
-        check(opt, ghOpts[opt]);
+      for (var opt in ghOpts) {
+        if (ghOpts.hasOwnProperty(opt)) {
+          check(opt, ghOpts[opt]);
+        }
       }
-    }
-    function check(key, val) {
-      it('should set ' + opt + ' to ' + val, function () {
-        converter.getOption(key).should.equal(val);
-      });
-    }
+      function check(key, val) {
+        it('should set ' + opt + ' to ' + val, function () {
+          converter.getOption(key).should.equal(val);
+        });
+      }
+    });
   });
 
   describe('extension methods', function () {
@@ -86,4 +92,41 @@ describe('showdown.Converter', function () {
       showdown.resetExtensions();
     });
   });
+
+  describe('events', function () {
+    var events = [
+          'anchors',
+          'autoLinks',
+          'blockGamut',
+          'blockQuotes',
+          'codeBlocks',
+          'codeSpans',
+          'githubCodeBlocks',
+          'headers',
+          'images',
+          'italicsAndBold',
+          'lists',
+          'paragraph',
+          'spanGamut'
+          //'strikeThrough',
+          //'tables'
+        ];
+
+    for (var i = 0; i < events.length; ++i) {
+      runListener(events[i] + '.before');
+      runListener(events[i] + '.after');
+    }
+
+    function runListener (name) {
+      it('should listen to ' + name, function () {
+        var converter = new showdown.Converter();
+        converter.listen(name, function (evtName, text, options) {
+          evtName.should.equal(name);
+          text.should.match(/^[\s\S]*foo[\s\S]*$/);
+          return text;
+        })
+          .makeHtml('foo');
+      });
+    }
+  });
 });

+ 22 - 1
test/node/showdown.js

@@ -46,6 +46,18 @@ describe('showdown.extension()', function () {
       showdown.extension('foo').should.eql([extObjMock]);
       showdown.resetExtensions();
     });
+
+    it('a listener extension', function () {
+      showdown.extension('foo', {
+        type: 'listener',
+        listeners: {
+          foo: function (name, txt) {
+            return txt;
+          }
+        }
+      });
+      showdown.resetExtensions();
+    });
   });
 
   describe('should refuse to register', function () {
@@ -62,7 +74,7 @@ describe('showdown.extension()', function () {
           type: 'foo'
         });
       };
-      expect(fn).to.throw(/type .+? is not recognized\. Valid values: "lang" or "output"/);
+      expect(fn).to.throw(/type .+? is not recognized\. Valid values: "lang\/language", "output\/html" or "listener"/);
     });
 
     it('an extension without regex or filter', function () {
@@ -73,6 +85,15 @@ describe('showdown.extension()', function () {
       };
       expect(fn).to.throw(/extensions must define either a "regex" property or a "filter" method/);
     });
+
+    it('a listener extension without a listeners property', function () {
+      var fn = function () {
+        showdown.extension('foo', {
+          type: 'listener'
+        });
+      };
+      expect(fn).to.throw(/Extensions of type "listener" must have a property called "listeners"/);
+    });
   });
 });
 

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott