Commits

Michael Shepanski committed 54e81ba

Huge refactor of code includes:
Better OO design.
Easier definition of Markup languages.
Now including Wikimedia and BBCode markup languages.
Fixed bugs in IE causing things to act wonky.

Comments (0)

Files changed (15)

 Primary Authors:
-    * Michael Shepanski
+  * Michael Shepanski
     * Redistributions in binary form must reproduce the above copyright notice,
       this list of conditions and the following disclaimer in the documentation
       and/or other materials provided with the distribution.
-    * Neither the name django-mingus nor the names of its contributors
+    * Neither the name jquery-wysiwym nor the names of its contributors
       may be used to endorse or promote products derived from this software without
       specific prior written permission.
 
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 REQUIREMENTS
 ------------
-* jquery-1.4.4.js or higher (required).
-* showdown-0.9.js or higher (required for live preview).
+* jquery-1.4.4.js or higher.
 
 
 INSTALL
     <link type='text/css' rel='stylesheet' href='/media/js/wysiwym/wysiwym.css'/>
 
 3.) Hook in the script by calling .wysiwym() on the textarea element
-    in your page.  That's It! ;)
+    in your page. That's It!:
+    $('#mytextarea').wysiwym(WysiwymMarkdown);
     $('#mytextarea').wysiwym(WysiwymMarkdown, options);
 
 
 following attributes:
 
 theme           - Color theme to use (light or dark); Default: 'light'.
+buttonContainer - jQuery element to place buttons; Default: undefined (auto-created).
 helpEnabled     - Set false to disable the help menu; Default: true.
 helpContainer   - jQuery element to place help; Default: undefined (auto-created).
 helpFloat       - CSS float value for helpContainer and helpToggle link; Default 'left'.
 helpToggle      - Set false to always show the help menu (disable toggle); Default: true.
 helpTextShow    - Toggle text to display when help is not visible; Default: 'show markup syntax'.
 helpTextHide    - Toggle text to display when help is visible;  Default: 'hide markup syntax'.
-buttonContainer - jQuery element to place buttons; Default: undefined (auto-created).

examples/basic.bbcode.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='examples.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+
+        // Sets up the Wysiwym editor given a markup language Current markup
+        // languages include: Wysiwym.Markup, Wysiwym.MediaWiki, Wysiwym.BBCode
+        // For a list of other options see the README.txt.
+        $('#mytextarea').wysiwym(Wysiwym.BBCode, {
+            helpEnabled: true,
+            helpToggle: true
+        });
+
+      });
+    </script>
+  </head>
+  <body>
+    <div id='container'>
+      <h1>Wysiwym BBCode Editor</h1>
+      <p>The editor on this page is using wysiwym. The library lends itself to be easily adapted
+        to other markup languages not currently implemented. If you write an extention for
+        another markup language and would like to help out with this package, I would love to
+        include it in future releases. You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <p>Examples:
+        <a href='basic.markdown.html'>Markdown</a>,
+        <a href='basic.mediawiki.html'>Media Wiki</a>,
+        <a href='basic.bbcode.html'>BBCode</a>,
+        <a href='theme.dark.html'>Dark Theme</a>
+        (<a href='devel.markdown.html'>Devel</a>)
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+  </body>
+</html>

examples/basic.dark.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html>
-  <head>
-    <title>Basic Example - jquery-wysiwym</title>
-    <link type='text/css' rel='stylesheet' href='example.css'/>
-    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
-    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
-    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
-    <script type='text/javascript' src='../wysiwym/markdown.js'></script>
-    <script type='text/javascript'>
-      $(function() {
-        $('#mytextarea').wysiwym(WysiwymMarkdown, {
-            theme: 'dark',
-            helpEnabled: true,
-            helpToggle: true
-        });
-      });
-    </script>
-  </head>
-  <body class='exampledark'>
-    <div id='container'>
-      <h1>Basic Wysiwym Markdown Editor</h1>
-      <p>The editor on this page is using wysiwym. Currently the only markup language
-        supported is Markdown. However, the library lends itself to be easily adapted
-        to other markup languages. If you write an extention for another markup
-        language and would like to help out with this package, I would love to
-        include it in future releases.  You can download or contibute via the
-        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
-      </p>
-      <textarea id='mytextarea'></textarea>
-    </div>
-  </body>
-</html>

examples/basic.light.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html>
-  <head>
-    <title>Basic Example - jquery-wysiwym</title>
-    <link type='text/css' rel='stylesheet' href='example.css'/>
-    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
-    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
-    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
-    <script type='text/javascript' src='../wysiwym/markdown.js'></script>
-    <script type='text/javascript'>
-      $(function() {
-        $('#mytextarea').wysiwym(WysiwymMarkdown, {
-            theme: 'light',
-            helpEnabled: true,
-            helpToggle: true
-        });
-      });
-    </script>
-  </head>
-  <body>
-    <div id='container'>
-      <h1>Basic Wysiwym Markdown Editor</h1>
-      <p>The editor on this page is using wysiwym. Currently the only markup language
-        supported is Markdown. However, the library lends itself to be easily adapted
-        to other markup languages. If you write an extention for another markup
-        language and would like to help out with this package, I would love to
-        include it in future releases.  You can download or contibute via the
-        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
-      </p>
-      <textarea id='mytextarea'></textarea>
-    </div>
-  </body>
-</html>

examples/basic.markdown.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='examples.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+
+        // Sets up the Wysiwym editor given a markup language Current markup
+        // languages include: Wysiwym.Markup, Wysiwym.MediaWiki, Wysiwym.BBCode
+        // For a list of other options see the README.txt.
+        $('#mytextarea').wysiwym(Wysiwym.Markdown, {
+            helpEnabled: true,
+            helpToggle: true
+        });
+
+      });
+    </script>
+  </head>
+  <body>
+    <div id='container'>
+      <h1>Wysiwym Markdown Editor</h1>
+      <p>The editor on this page is using wysiwym. The library lends itself to be easily adapted
+        to other markup languages not currently implemented. If you write an extention for
+        another markup language and would like to help out with this package, I would love to
+        include it in future releases. You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <p>Examples:
+        <a href='basic.markdown.html'>Markdown</a>,
+        <a href='basic.mediawiki.html'>Media Wiki</a>,
+        <a href='basic.bbcode.html'>BBCode</a>,
+        <a href='theme.dark.html'>Dark Theme</a>
+        (<a href='devel.markdown.html'>Devel</a>)
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+  </body>
+</html>

examples/basic.mediawiki.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='examples.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+
+        // Sets up the Wysiwym editor given a markup language Current markup
+        // languages include: Wysiwym.Markup, Wysiwym.MediaWiki, Wysiwym.BBCode
+        // For a list of other options see the README.txt.
+        $('#mytextarea').wysiwym(Wysiwym.Mediawiki, {
+            helpEnabled: true,
+            helpToggle: true
+        });
+
+      });
+    </script>
+  </head>
+  <body>
+    <div id='container'>
+      <h1>Wysiwym MediaWiki Editor</h1>
+      <p>The editor on this page is using wysiwym. The library lends itself to be easily adapted
+        to other markup languages not currently implemented. If you write an extention for
+        another markup language and would like to help out with this package, I would love to
+        include it in future releases. You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <p>Examples:
+        <a href='basic.markdown.html'>Markdown</a>,
+        <a href='basic.mediawiki.html'>Media Wiki</a>,
+        <a href='basic.bbcode.html'>BBCode</a>,
+        <a href='theme.dark.html'>Dark Theme</a>
+        (<a href='devel.markdown.html'>Devel</a>)
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+  </body>
+</html>

examples/devel.markdown.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='examples.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+
+        // Sets up the Wysiwym editor given a markup language Current markup
+        // languages include: Wysiwym.Markup, Wysiwym.MediaWiki, Wysiwym.BBCode
+        // For a list of other options see the README.txt.
+        $('#mytextarea').wysiwym(Wysiwym.Markdown, {
+            helpEnabled: true,
+            helpToggle: true
+        });
+
+        // Update the Debug Text every half second
+        var prevDebugstr = '';
+        var updateTextareaDebug = function() {
+            debugstr = new Wysiwym.Textarea($('#mytextarea')).toString();
+            if (debugstr != prevDebugstr) {
+                prevDebugstr = debugstr;
+                $('#textarea-debug').text(debugstr);
+            }
+            setTimeout(updateTextareaDebug, 300);
+        }
+        updateTextareaDebug();
+
+      });
+    </script>
+  </head>
+  <body>
+    <div id='container'>
+      <h1>Wysiwym Development Editor</h1>
+      <p>The editor on this page is using wysiwym. The library lends itself to be easily adapted
+        to other markup languages not currently implemented. If you write an extention for
+        another markup language and would like to help out with this package, I would love to
+        include it in future releases. You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <p>Examples:
+        <a href='basic.markdown.html'>Markdown</a>,
+        <a href='basic.mediawiki.html'>MediaWiki</a>,
+        <a href='basic.bbcode.html'>BBCode</a>,
+        <a href='theme.dark.html'>Dark Theme</a>
+        (<a href='devel.markdown.html'>Devel</a>)
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+    <div style='clear:both;'></div>
+    <pre id='textarea-debug'>Hello</pre>
+  </body>
+</html>

examples/example.css

-/*-------------------------------------------------------------------
- * Example CSS
- * NOTE: These styles are for the demo page, which have nothing
- * to do with the actual styles for the wysiwym editor. If you
- * are looking for the light, dark themes for the wysiwym button
- * and help menu, you should look in wysiwym.css.
- *---------------------------------------------------------------- */
-body {
-  background-color: #f5f5f5;
-  font-family: tahoma, verdana, arial;
-  font-size: 14px;
-  color: #666;
-}
-a {
-  color: #a50;
-  text-decoration: none;
-}
-a:hover {
-  color: #292;
-  text-decoration: underline;
-}
-#container {
-  width: 800px;
-}
-h1 {
-  font-size: 24px;
-  font-weight: bold;
-  color: #555;
-  margin-bottom: 10px;
-}
-p {
-  margin: 10px 0px 20px 0px;
-}
-textarea {
-  display: block;
-  width: 500px;
-  height: 150px;
-  padding: 3px;
-  margin-top: 3px;
-  font-family: Consolas, Menlo, Monaco, 'Lucida Console', monospace, serif;
-}
-
-/*--- Demo dark theme ---*/
-body.exampledark {
-  background-color: #050505;
-  color: #777;
-}
-body.exampledark h1 {
-  color: #aaa;
-}
-body.exampledark textarea {
-  background-color: #282D31;
-  border-width: 0px;
-  color: #ccc;
-}

examples/examples.css

+/*-------------------------------------------------------------------
+ * Example CSS
+ * NOTE: These styles are for the demo page, which have nothing
+ * to do with the actual styles for the wysiwym editor. If you
+ * are looking for the light, dark themes for the wysiwym button
+ * and help menu, you should look in wysiwym.css.
+ *---------------------------------------------------------------- */
+body {
+  background-color: #f5f5f5;
+  font-family: tahoma, verdana, arial;
+  font-size: 14px;
+  color: #666;
+}
+a {
+  color: #a50;
+  text-decoration: none;
+}
+a:hover,
+a:visited {
+  color: #292;
+  text-decoration: underline;
+}
+#container {
+  width: 700px;
+}
+h1 {
+  font-size: 24px;
+  font-weight: bold;
+  color: #555;
+  margin-bottom: 10px;
+}
+p {
+  margin: 10px 0px 20px 0px;
+}
+textarea {
+  display: block;
+  width: 500px;
+  height: 150px;
+  padding: 3px;
+  margin-top: 3px;
+  font-size: 12px;
+  font-family: 'Liberation Mono', Consolas, Menlo, Monaco, 'Lucida Console', monospace, serif;
+}
+
+/*--- Demo dark theme ---*/
+body.exampledark {
+  background-color: #050505;
+  color: #777;
+}
+body.exampledark h1 {
+  color: #aaa;
+}
+body.exampledark textarea {
+  background-color: #282D31;
+  border-width: 0px;
+  color: #ccc;
+}
+
+/*--- Textarea Status ---*/
+#textarea-debug {
+  clear: both;
+  width: 500px;
+  padding: 4px;
+  margin-top: 5px;
+  color: #333;
+  font-size: 12px;
+  font-family: 'Liberation Mono', Consolas, Menlo, Monaco, 'Lucida Console', monospace, serif;
+  background-color: #fff;
+}

examples/theme.dark.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='examples.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+
+        // Sets up the Wysiwym editor given a markup language Current markup
+        // languages include: Wysiwym.Markup, Wysiwym.MediaWiki, Wysiwym.BBCode
+        // For a list of other options see the README.txt.
+        $('#mytextarea').wysiwym(Wysiwym.Markdown, {
+            theme: 'dark',
+            helpEnabled: true,
+            helpToggle: true
+        });
+
+      });
+    </script>
+  </head>
+  <body class='exampledark'>
+    <div id='container'>
+      <h1>Wysiwym Dark-Theme Editor</h1>
+      <p>The editor on this page is using wysiwym. The library lends itself to be easily adapted
+        to other markup languages not currently implemented. If you write an extention for
+        another markup language and would like to help out with this package, I would love to
+        include it in future releases. You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <p>Examples:
+        <a href='basic.markdown.html'>Markdown</a>,
+        <a href='basic.mediawiki.html'>Media Wiki</a>,
+        <a href='basic.bbcode.html'>BBCode</a>,
+        <a href='theme.dark.html'>Dark Theme</a>
+        (<a href='devel.markdown.html'>Devel</a>)
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+  </body>
+</html>

wysiwym/markdown.js

-/* ---------------------------------------------------------------------------
- * Markdown Button Set for Wysiwym Editor
- *
- * Created 2009, Michael Shepanski
- * Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States.
- * http://creativecommons.org/licenses/by-nc-sa/3.0/us/
- *
- * Instructions:
- *   1. Follow all instructions from the main ajaxcomments and
- *      jquery.wysiwym.js files.
- *
- *   2. Add the following references to html template containing Django's
- *      Comment application:
- *        * ajaxcomments/wysiwym/markdown/wysiwym.markdown.css
- *        * ajaxcomments/wysiwym/markdown/wysiwym.markdown.js
- *        * ajaxcomments/wysiwym/markdown/showdown.js
- *---------------------------------------------------------------------------- */
-
-function WysiwymMarkdown(textarea) {
-
-    // Public Variables
-    this.name = 'Markdown';          // Markup Language Name
-    this.buttonLink  = buttonLink;   // Link Callback
-    this.genericSpan = genericSpan;  // Public Callback function
-    this.genericLine = genericLine;  // Public Callback function
-
-    // Private Markdown Variables
-    var prefixRegex = /^[\s\>\.\*\+\-0-9]+\s/;
-    var lineTypes = {
-        BULLET: { prefix: '* ' },
-        NUMBER: { prefix: '# ' },
-        QUOTE:  { prefix: '> ' },
-        CODE:   { prefix: '    ' }
-    };
-
-    // Required Variable (Buttons to Display)
-    this.buttons = [
-        { name: 'Bold',        callback: this.genericSpan, args: {wrapText: '**', dummyText: 'strong text'} },
-        { name: 'Italic',      callback: this.genericSpan, args: {wrapText: '_',  dummyText: 'emphasized text'} },
-        { name: 'Link',        callback: this.buttonLink },
-        { name: 'Bullet List', callback: this.genericLine, args: {lineType: lineTypes.BULLET} },
-        { name: 'Quote',       callback: this.genericLine, args: {lineType: lineTypes.QUOTE} },
-        { name: 'Code',        callback: this.genericLine, args: {lineType: lineTypes.CODE} },
-    ];
-
-    // Used for display purposes only;  Provides a quick reference
-    // for people to see the syntax for this markup language.
-    this.helpItems = [
-        { name: 'Header', syntax: '## Header ##' },
-        { name: 'Bold',   syntax: '**bold**' },
-        { name: 'Italic', syntax: '_italics_' },
-        { name: 'Link',   syntax: '[pk!](http://google.com)' },
-        { name: 'List Item', syntax: '* list item' },
-        { name: 'Blockquote', syntax: '&gt; quoted text' },
-        { name: 'Large Code Block', syntax: '(Begin lines with 4 spaces)' },
-        { name: 'Inline Code Block', syntax: '&lt;code&gt;inline code&lt;/code&gt;' }
-    ]
-
-    // Register Return Line (for auto-indent)
-    $(textarea).bind('keydown', function(event) {
-        if (event.keyCode == 13)
-            return autoIndent();
-        return true;
-    });
-
-    /*-------------------------------------------------------------
-     * buttonLink:  Wraps the Selection with the specified
-     *   markdown link syntax, [This link](http://example.net/)
-     *------------------------------------------------------------- */
-    function buttonLink(event) {
-        var wtext = new WysiwymTextarea(textarea);
-        var cursor = wtext.getCursor();
-        var linkUrl = null;
-        // Set the LinkUrl if this is a proper Link
-        if (wtext.selectionIsWrapped("\[", "](")) {
-            var linkSuffix = wtext.getLine(cursor.end.line).text.substring(cursor.end.position);
-            var matches = linkSuffix.match(/^\]\(.*?\)/);
-            if (matches != null)
-                linkUrl = matches[0].substring(2, matches[0].length-1);
-        }
-        // Update the Textarea
-        if (linkUrl != null) {
-            wtext.unwrapSelection("[", "]("+linkUrl+")");
-        } else if (wtext.getSelectionLength() == 0) {
-            wtext.appendToSelection("Link Title");
-            wtext.wrapSelection("[", "](http://example.com)");
-        } else {
-            wtext.wrapSelection("[", "](http://example.com)");
-        }
-        wtext.update();
-    }
-
-    /*-------------------------------------------------------------
-     * genericSpan:  Wraps the Selection with the specified
-     *   args.wrapText defined on the button.
-     *------------------------------------------------------------- */
-    function genericSpan(event) {
-        var wrapText = event.data.args.wrapText;
-        var dummyText = event.data.args.dummyText;
-        var wtext = new WysiwymTextarea(textarea);
-        // Update the Textarea
-        if (wtext.selectionIsWrapped(wrapText)) {
-            wtext.unwrapSelection(wrapText);
-        } else if (wtext.getSelectionLength() == 0) {
-            wtext.appendToSelection(dummyText);
-            wtext.wrapSelection(wrapText);
-        } else {
-            wtext.wrapSelection(wrapText);
-        }
-        wtext.update();
-    }
-
-    /*-------------------------------------------------------------
-     * buttonLine:  Starts the Lines with the specified
-     *   args.prefixText defined on the button definition
-     *------------------------------------------------------------- */
-    function genericLine(event) {
-        var lineType = event.data.args.lineType;
-        var wtext = new WysiwymTextarea(textarea);
-        // Update the Textarea
-        if (!wtext.selectionLinesHavePrefix(lineType.prefix)) {
-            // Not all Lines are LineType; Remove all prefixes and Add LineType's prefix
-            var selectRange = wtext.getSelectionRange();
-            for (i=selectRange[0]; i<=selectRange[1]; i++) {
-                var lineInfo = getLineProperties(wtext, i);
-                wtext.removeLinePrefix(i, lineInfo.prefix);
-                wtext.insertLinePrefix(i, lineType.prefix);
-            }
-            // Check Blank Lines Around Selection
-            assertBlockWrap(wtext, lineType);
-        } else {
-            // All Lines are LineType; Remove all prefixes
-            wtext.removeSelectionLinesPrefix(lineType.prefix);
-        }
-        wtext.update();
-    }
-
-    /*-------------------------------------------------------------
-     * getLineProperties:  Returns information about a single line
-     *------------------------------------------------------------- */
-    function getLineProperties(wtext, lineNum) {
-        var line = wtext.getLine(lineNum);
-        for (var key in lineTypes) {
-            var type = lineTypes[key];
-            var prefix = type.prefix;
-            if (wtext.lineHasPrefix(lineNum, prefix)) {
-                var fullPrefix = line.text.match(prefixRegex)[0];
-                return { prefix:fullPrefix, text:line.text.substring(prefix, line.text.length) };
-            }
-        }
-        return { prefix:'', text:line.text };
-    }
-
-    /*-------------------------------------------------------------
-     * assertBlockWrap:  Asserts the selected lines have a line
-     *   with the specified prefix before and after.  If not, a
-     *   blank line is inserted to ensure the lineType is applied
-     *   correctly.
-     *------------------------------------------------------------- */
-    function assertBlockWrap(wtext, lineType) {
-        var cursor = wtext.getCursor();
-        // Check the Previous Line
-        if (cursor.start.line != 0) {
-            var lineInfo = getLineProperties(wtext, cursor.start.line-1);
-            if ((lineInfo.prefix != lineType.prefix) && (lineInfo.text.trim() != ""))
-                wtext.insertLinePrefix(cursor.start.line, wtext.NEWLINE);
-        }
-        // Check the Next Line
-        if (cursor.end.line != wtext.getNumLines()-1) {
-            var lineInfo = getLineProperties(wtext, cursor.end.line+1);
-            if ((lineInfo.prefix != lineType.prefix) && (lineInfo.text.trim() != ""))
-                wtext.insertLineSuffix(cursor.end.line, wtext.NEWLINE);
-        }
-    }
-
-    /*-------------------------------------------------------------
-     * autoIndent:  Auto-Indent the next line when enter pushed.
-     *------------------------------------------------------------- */
-    function autoIndent() {
-        var wtext = new WysiwymTextarea(textarea);
-        var cursor = wtext.getCursor();
-        var lineNum = cursor.start.line;
-        var line = wtext.getLine(lineNum);
-        var lineText = line.text.substring(0, cursor.start.position);
-        // Make sure there is Prefix Text
-        var matches = lineText.match(prefixRegex);
-        if (matches == null) { return true; }
-        // If there is no Actual Text, Clear the line
-        var prefix = matches[0].substring(0, matches[0].length);
-        if (prefix.length == cursor.start.position) {
-            // Return on Blank Indented Line (Clear Prefix)
-            wtext.removeLinePrefix(lineNum, prefix);
-            wtext.update();
-            return true;
-        } else {
-            // Normal Auto-Indent
-            wtext.insertSelectionPrefix(wtext.NEWLINE + prefix);
-            wtext.update();
-            return false;
-        }
-    }
-
-}

wysiwym/wysiwym.css

   background-repeat: repeat-x;
   background-position: 0px 0px;
   border-right: 1px solid #aaa;
+
 }
-div.wysiwymButtons div.button:first-child {
+div.wysiwymButtons div.button.first {
   border-top-left-radius: 3px;
   border-bottom-left-radius: 3px;
   -webkit-border-top-left-radius: 3px;
   -moz-border-radius-topleft: 3px;
   -moz-border-radius-bottomleft: 3px;
 }
-div.wysiwymButtons div.button:last-child {
+div.wysiwymButtons div.button.last {
   border-right: 0px;
   border-top-right-radius: 3px;
   border-bottom-right-radius: 3px;
   -moz-border-radius-bottomright: 3px;
 }
 div.wysiwymButtons div.button span.wrap {
-  width: 22px;
+  display: block;
   height: 18px;
-  display: block;
-  background-image: url(buttons.light.png);
+}
+div.wysiwymButtons div.button span.text {
+  padding: 0px 4px;
+  color: #555;
+  font-size: 12px;
+  font-weight: bold;
+  line-height: 16px;
+  cursor: default;
+  user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -o-user-select: none;
+  -webkit-user-select: none;
 }
 
 /*--- Buttons for dark theme. ---*/
   background-image: url(buttons.dark.png);
 }
 
-/*--- Button Image Offsets (Normal) ---*/
-div.wysiwymButtons div.button span.text            { display: none; }
-div.wysiwymButtons div.button-bold span.wrap       { background-position: -1px -54px; }
-div.wysiwymButtons div.button-italic span.wrap     { background-position: -1px -81px; }
-div.wysiwymButtons div.button-link span.wrap       { background-position: -1px -106px; }
-div.wysiwymButtons div.button-bulletlist span.wrap { background-position: -1px -132px; }
-div.wysiwymButtons div.button-numberlist span.wrap { background-position: -1px -159px; }
-div.wysiwymButtons div.button-quote span.wrap      { background-position: -1px -184px; }
-div.wysiwymButtons div.button-code span.wrap       { background-position: -1px -211px; }
+/*--- Disable Text for all known button types ---*/
+div.wysiwymButtons div.button.bold span.text       { display: none; }
+div.wysiwymButtons div.button.italic span.text     { display: none; }
+div.wysiwymButtons div.button.link span.text       { display: none; }
+div.wysiwymButtons div.button.bulletlist span.text { display: none; }
+div.wysiwymButtons div.button.numberlist span.text { display: none; }
+div.wysiwymButtons div.button.quote span.text      { display: none; }
+div.wysiwymButtons div.button.code span.text       { display: none; }
+
+/*--- Button Image Offsets (Default) ---*/
+div.wysiwymButtons div.button.bold span.wrap       { width: 22px; background-image: url(buttons.light.png); background-position: -1px -54px; }
+div.wysiwymButtons div.button.italic span.wrap     { width: 22px; background-image: url(buttons.light.png); background-position: -1px -81px; }
+div.wysiwymButtons div.button.link span.wrap       { width: 22px; background-image: url(buttons.light.png); background-position: -1px -106px; }
+div.wysiwymButtons div.button.bulletlist span.wrap { width: 22px; background-image: url(buttons.light.png); background-position: -1px -132px; }
+div.wysiwymButtons div.button.numberlist span.wrap { width: 22px; background-image: url(buttons.light.png); background-position: -2px -158px; }
+div.wysiwymButtons div.button.quote span.wrap      { width: 22px; background-image: url(buttons.light.png); background-position: -1px -184px; }
+div.wysiwymButtons div.button.code span.wrap       { width: 22px; background-image: url(buttons.light.png); background-position: -1px -211px; }
+
+/*--- Button Image Offsets (Dark) ---*/
+div.wysiwymButtons.dark div.button.bold span.wrap       { width: 22px; background-image: url(buttons.dark.png); background-position: -1px -54px; }
+div.wysiwymButtons.dark div.button.italic span.wrap     { width: 22px; background-image: url(buttons.dark.png); background-position: -1px -81px; }
+div.wysiwymButtons.dark div.button.link span.wrap       { width: 22px; background-image: url(buttons.dark.png); background-position: -1px -106px; }
+div.wysiwymButtons.dark div.button.bulletlist span.wrap { width: 22px; background-image: url(buttons.dark.png); background-position: -1px -132px; }
+div.wysiwymButtons.dark div.button.numberlist span.wrap { width: 22px; background-image: url(buttons.dark.png); background-position: -2px -158px; }
+div.wysiwymButtons.dark div.button.quote span.wrap      { width: 22px; background-image: url(buttons.dark.png); background-position: -1px -184px; }
+div.wysiwymButtons.dark div.button.code span.wrap       { width: 22px; background-image: url(buttons.dark.png); background-position: -1px -211px; }
+
 
 /*--- Button Images Offsets (Hover) ---*/
 div.wysiwymButtons div.button:hover                      { background-position: 0px -30px; }
-div.wysiwymButtons div.button-bold:hover span.wrap       { background-position: -27px -54px; }
-div.wysiwymButtons div.button-italic:hover span.wrap     { background-position: -27px -81px; }
-div.wysiwymButtons div.button-link:hover span.wrap       { background-position: -27px -106px; }
-div.wysiwymButtons div.button-bulletlist:hover span.wrap { background-position: -27px -132px; }
-div.wysiwymButtons div.button-numberlist:hover span.wrap { background-position: -27px -159px; }
-div.wysiwymButtons div.button-quote:hover span.wrap      { background-position: -27px -184px; }
-div.wysiwymButtons div.button-code:hover span.wrap       { background-position: -27px -211px; }
+div.wysiwymButtons div.button.bold:hover span.wrap       { background-position: -27px -54px; }
+div.wysiwymButtons div.button.italic:hover span.wrap     { background-position: -27px -81px; }
+div.wysiwymButtons div.button.link:hover span.wrap       { background-position: -27px -106px; }
+div.wysiwymButtons div.button.bulletlist:hover span.wrap { background-position: -27px -132px; }
+div.wysiwymButtons div.button.numberlist:hover span.wrap { background-position: -28px -158px; }
+div.wysiwymButtons div.button.quote:hover span.wrap      { background-position: -27px -184px; }
+div.wysiwymButtons div.button.code:hover span.wrap       { background-position: -27px -211px; }
 
 
 /*-------------------------------------------------------------
   line-height: 18px;
 }
 div.wysiwymHelp {
-  padding: 1px 5px;
+  padding: 0px 5px;
   line-height: 18px;
   font-size: 10px;
   background-color: #eee;

wysiwym/wysiwym.js

 /*----------------------------------------------------------------------------------------------
  * Simple Wysiwym Editor for jQuery
- *   * wysiwym(markupSet, id)
- *   * WysiwymTextarea(textarea)
+ * Version: 2.0 (2011-01-26)
  *--------------------------------------------------------------------------------------------- */
+var BLANKLINE = '';
+var Wysiwym = {};
+
+$.fn.wysiwym = function(markupSet, options) {
+    this.EDITORCLASS = 'wysiwymEditor';          // Class to use for the wysiwym editor
+    this.BUTTONCLASS = 'wysiwymButtons';         // Class to use for the wysiwym buttons
+    this.HELPCLASS = 'wysiwymHelp';              // Class to use for the wysiwym help
+    this.HELPTOGGLECLASS = 'wysiwymHelpToggle';  // Class to use for the wysiwym help
+    this.textelem = this;                        // Javascript textarea element
+    this.textarea = $(this);                     // jQuery textarea object
+    this.editor = undefined;                     // jQuery div wrapper around this editor
+    this.markup = new markupSet(this);           // Wysiwym Markup set to use
+    this.defaults = {                            // Default option values
+        buttonContainer: undefined,              // jQuery elem to place buttons (makes one by default)
+        helpEnabled: true,                       // Set true to display the help dropdown
+        helpContainer: undefined,                // jQuery elem to place help (makes one by default)
+        helpFloat: 'left',                       // CSS float value for helpContainer and helpToggle link
+        helpToggle: true,                        // Set true to use a toggle link for help
+        helpTextShow: 'show markup syntax',      // Toggle text to display when help is not visible
+        helpTextHide: 'hide markup syntax',      // Toggle text to display when help is visible
+        theme: 'light'                           // Color theme to use ('light' or  'dark')
+    };
+    this.options = $.extend(this.defaults, options ? options : {});
+
+    // Add the button container and all buttons
+    this.initializeButtons = function() {
+        var markup = this.markup;
+        if (this.options.buttonContainer == undefined)
+            this.options.buttonContainer = $("<div></div>").insertBefore(this.textarea);
+        this.options.buttonContainer.addClass(this.BUTTONCLASS);
+        this.options.buttonContainer.addClass(this.options.theme);
+        for (var i=0; i<markup.buttons.length; i++) {
+            // Create the button and apply first / last classes
+            var button = markup.buttons[i];
+            var jqbutton = button.create();
+            if (i == 0) { jqbutton.addClass('first'); }
+            if (i == markup.buttons.length-1) { jqbutton.addClass('last'); }
+            // Bind the button data and click event callback
+            var data = $.extend({markup:this.markup}, button.data);
+            jqbutton.bind('click', data, button.callback);
+            this.options.buttonContainer.append(jqbutton);
+        }
+    };
+
+    // Initialize the AutoIndent trigger
+    this.initializeAutoIndent = function() {
+        if (this.markup.autoindents) {
+            var data = {markup:this.markup};
+            this.textarea.bind('keydown', data, Wysiwym.autoIndent);
+        }
+    };
+
+    // Initialize the help syntax dropdown
+    this.initializeHelp = function() {
+        this.options.helpContainer = options.helpContainer;
+        if (this.options.helpContainer == undefined)
+            this.options.helpContainer = $("<div></div>").insertAfter(this.textarea);
+        this.options.helpContainer.addClass(this.HELPCLASS);
+        this.options.helpContainer.addClass(this.options.theme);
+        // Add the help table and items
+        var helpBody = $('<tbody></tbody>');
+        var helpTable = $('<table cellpadding="0" cellspacing="0" border="0"></table>').append(helpBody);
+        for (var i=0; i<this.markup.help.length; i++) {
+            var item = this.markup.help[i];
+            helpBody.append('<tr><th>'+ item.label +'</th><td>'+ item.syntax +'</td></tr>');
+        };
+        this.options.helpContainer.append(helpTable);
+    };
+
+    // Initialize the Help Toggle Button
+    this.initializeHelpToggle = function() {
+        var options = this.options;
+        if (options.helpToggle) {
+            var helpToggle = $("<a href='#'>"+ options.helpTextShow +"</a>");
+            helpToggle.addClass(this.HELPTOGGLECLASS);
+            if (options.helpFloat) {
+                helpToggle.css('float', this.options.helpFloat);
+                options.helpContainer.css({'float':options.helpFloat, 'clear':options.helpFloat});
+            }
+            helpToggle.bind('click', function() {
+                if (options.helpContainer.is(':visible')) {
+                    options.helpContainer.slideUp('fast');
+                    $(this).text(options.helpTextShow);
+                } else {
+                    options.helpContainer.slideDown('fast');
+                    $(this).text(options.helpTextHide);
+                }
+                return false;
+            });
+            options.helpContainer.before(helpToggle).hide();
+        }
+    };
+
+    // Initialize the Wysiwym Editor
+    this.editor = $('<div class="'+ this.EDITORCLASS +'"></div>');
+    this.textarea.wrap(this.editor);
+    this.initializeButtons();
+    this.initializeAutoIndent();
+    this.initializeHelp();
+    this.initializeHelpToggle();
+};
+
 
 /*----------------------------------------------------------------------------------------------
- * wysiwym(markupSet, id, buttons)
- * @param markupSet:  (class)  Class definition of the markup set to use for this editor.
- * @param id:         (string) Optionally specify a unique ID for this editor.
- * @param buttons:    (jqElem) Optionally specify an element to place buttons into.
+ * Wysiwym Selection
+ * Manipulate the textarea selection
  *--------------------------------------------------------------------------------------------- */
-$.fn.wysiwym = function(markupSet, options) {
-    if (options == undefined) { options = {}; }
+Wysiwym.Selection = function(wysiwym) {
+    this.lines = wysiwym.lines;                 // Reference to wysiwym.lines
+    this.start = { line:0, position:0 },        // Current cursor start positon
+    this.end = { line:0, position:0 },          // Current cursor end position
 
-    // Setup the Default Options
-    var defaults = {}
-    defaults.theme = 'light';                     // Color theme to use ('light' or  'dark')
-    defaults.helpEnabled = true;                  // Set true to display the help dropdown
-    defaults.helpContainer = undefined;           // jQuery elem to place help (makes one by default)
-    defaults.helpFloat = 'left';                  // CSS float value for helpContainer and helpToggle link
-    defaults.helpToggle = true;                   // Set true to use a toggle link for help
-    defaults.helpTextShow = 'show markup syntax'; // Toggle text to display when help is not visible
-    defaults.helpTextHide = 'hide markup syntax'; // Toggle text to display when help is visible
-    defaults.buttonContainer = undefined;         // jQuery elem to place buttons (makes one by default)
-    options = $.extend(defaults, options);        // Merge options with defaults
+    // Return a string representation of this object.
+    this.toString = function() {
+        var str = 'SELECTION: '+ this.length() +' chars\n';
+        str += 'START LINE: '+ this.start.line +'; POSITION: '+ this.start.position +'\n';
+        str += 'END LINE: '+ this.end.line +'; POSITION: '+ this.end.position +'\n';
+        return str;
+    };
 
-    // Setup the Wysiwym Editor
-    return this.each(function() {
-	var textarea = this;                      // Javascript Textarea Object
-	var jqTextarea = $(this);                 // jQuery Textarea Object
-	var markup = new markupSet(textarea);     // Markup Set to use for this Textarea
+    // Add a line prefix, reguardless if it's already set or not.
+    this.addLinePrefixes = function(prefix) {
+        for (var i=this.start.line; i <= this.end.line; i++) {
+            this.lines[i] = prefix + this.lines[i];
+        }
+        this.start.position += prefix.length;
+        this.end.position += prefix.length;
+    };
 
-	// Initialize the Basic Editor HTML
-	$(jqTextarea).wrap("<div class='wysiwymEditor'></div>");
-	var editor = jqTextarea.parent();
+    // Add the specified prefix to the selection
+    this.addPrefix = function(prefix) {
+        var line = this.lines[this.start.line];
+        var newline = line.substring(0, this.start.position) +
+            prefix + line.substring(this.start.position, line.length);
+        this.lines[this.start.line] = newline;
+        this.start.position += prefix.length;
+        if (this.start.line == this.end.line)
+            this.end.position += prefix.length;
+    };
 
-	// Add the Button Container and all it's Buttons
-	var buttonContainer = options.buttonContainer;
-	if (buttonContainer == undefined)
-	    buttonContainer = $("<div></div>").insertBefore(jqTextarea);
-	buttonContainer.addClass('wysiwymButtons');
-	buttonContainer.addClass(options.theme);
-	$.each(markup.buttons, function() {
-	    var cssClass = this.name.toLowerCase().replace(' ', '');
-	    var buttonHtml = "";
-	    buttonHtml += "<div class='button button-"+cssClass+"' title='"+this.name+"'>";
-	    buttonHtml += "<span class='wrap'><span class='text'>"+this.name+"</span></span>";
-	    buttonHtml += "</div>";
-	    var button = $(buttonHtml);
-	    var data = { editor:editor, textarea:textarea, button:button, args:this.args };
-	    button.bind('click', data, this.callback);
-	    buttonContainer.append(button);
-	});
+    // Add the specified suffix to the selection
+    this.addSuffix = function(suffix) {
+        var line = this.lines[this.end.line];
+        var newline = line.substring(0, this.end.position) +
+            suffix + line.substring(this.end.position, line.length);
+        this.lines[this.end.line] = newline;
+    };
 
-	// Include the Help Menu (optional)
-	if (options.helpEnabled) {
-	    var helpContainer = options.helpContainer;
-	    if (helpContainer == undefined)
-		helpContainer = $("<div></div>").insertAfter(jqTextarea);
-	    helpContainer.addClass('wysiwymHelp');
-	    helpContainer.addClass(options.theme);
-	    var helpTable = $("<table cellpadding='0' cellspacing='0' border='0'><tbody></tbody></table>");
-	    var helpBody = helpTable.children('tbody');
-	    $.each(markup.helpItems, function() {
-		helpBody.append("<tr><th>"+this.name+"</th><td>"+this.syntax+"</td></tr>");
-	    });
-	    helpContainer.append(helpTable);
-	    // Display Help Menu Toggle Link (optional)
-	    if (options.helpToggle) {
-		var helpToggle = $("<a href='#' class='wysiwymHelpToggle'>"+options.helpTextShow+"</a>");
-		helpToggle.bind('click', function () {
-		    if (helpContainer.is(':visible')) {
-			helpContainer.slideUp('fast');
-			$(this).text(options.helpTextShow);
-		    } else {
-			helpContainer.slideDown('fast');
-			$(this).text(options.helpTextHide);
-		    }
-		    return false;
-		});
-		if (options.helpFloat) {
-		    helpToggle.css('float', options.helpFloat);
-		    helpContainer.css({'float':options.helpFloat, 'clear':options.helpFloat});
-		}
-		helpContainer.before(helpToggle).hide();
-	    }
-	}
+    // Append the specified text to the selection
+    this.append = function(text) {
+        var line = this.lines[this.end.line];
+        var newline = line.substring(0, this.end.position) +
+            text + line.substring(this.end.position, line.length);
+        this.lines[this.end.line] = newline;
+        this.end.position += text.length;
+    };
 
-    });
+    // Return an array of lines in the selection
+    this.getLines = function() {
+        var selectedlines = [];
+        for (var i=this.start.line; i <= this.end.line; i++)
+            selectedlines[selectedlines.length] = this.lines[i];
+        return selectedlines;
+    };
+
+    // Return true if selected text contains has the specified prefix
+    this.hasPrefix = function(prefix) {
+        var line = this.lines[this.start.line];
+        var start = this.start.position - prefix.length;
+        if ((start < 0) || (line.substring(start, this.start.position) != prefix))
+            return false;
+        return true;
+    };
+
+    // Return true if selected text contains has the specified suffix
+    this.hasSuffix = function(suffix) {
+        var line = this.lines[this.end.line];
+        var end = this.end.position + suffix.length;
+        if ((end > line.length) || (line.substring(this.end.position, end) != suffix))
+            return false;
+        return true;
+    };
+
+    // Insert the line before the selection to the specified text. If force is
+    // set to false and the line is already set, it will be left alone.
+    this.insertPreviousLine = function(newline, force) {
+        force = force !== undefined ? force : true;
+        var prevnum = this.start.line - 1;
+        if ((force) || ((prevnum >= 0) && (this.lines[prevnum] != newline))) {
+            this.lines.splice(this.start.line, 0, newline);
+            this.start.line += 1;
+            this.end.line += 1;
+        }
+    };
+
+    // Insert the line after the selection to the specified text. If force is
+    // set to false and the line is already set, it will be left alone.
+    this.insertNextLine = function(newline, force) {
+        force = force !== undefined ? force : true;
+        var nextnum = this.end.line + 1;
+        if ((force) || ((nextnum < this.lines.length) && (this.lines[nextnum] != newline)))
+            this.lines.splice(nextnum, 0, newline);
+    };
+
+    // Return true if selected text is wrapped with prefix & suffix
+    this.isWrapped = function(prefix, suffix) {
+        return ((this.hasPrefix(prefix)) && (this.hasSuffix(suffix)));
+    };
+
+    // Return the selection length
+    this.length = function() {
+        return this.val().length;
+    };
+
+    // Return true if all lines have the specified prefix. Optionally
+    // specify prefix as a regular expression.
+    this.linesHavePrefix = function(prefix) {
+        for (var i=this.start.line; i <= this.end.line; i++) {
+            var line = this.lines[i];
+            if ((typeof(prefix) == 'function') && (!line.match(prefix))) {
+                return false;
+            } else if ((typeof(prefix) == 'string') && (!line.startswith(prefix))) {
+                return false;
+            }
+        }
+        return true;
+    };
+
+    // Prepend the specified text to the selection
+    this.prepend = function(text) {
+        var line = this.lines[this.start.line];
+        var newline = line.substring(0, this.start.position) +
+            text + line.substring(this.start.position, line.length);
+        this.lines[this.start.line] = newline;
+        // Update Class Variables
+        if (this.start.line == this.end.line)
+            this.end.position += text.length;
+    };
+
+    // Remove the prefix from each line in the selection. If the line
+    // does not contain the specified prefix, it will be left alone.
+    // Optionally specify prefix as a regular expression.
+    this.removeLinePrefixes = function(prefix) {
+        for (var i=this.start.line; i <= this.end.line; i++) {
+            var line = this.lines[i];
+            var match = prefix;
+            // Check prefix is a regex
+            if (typeof(prefix) == 'function')
+                match = line.match(prefix)[0];
+            // Do the replace
+            if (line.startswith(match)) {
+                this.lines[i] = line.substring(match.length, line.length);
+                if (i == this.start.line)
+                    this.start.position -= match.length;
+                if (i == this.end.line)
+                    this.end.position -= match.length;
+            }
+
+        }
+    };
+
+    // Remove the previous line. If regex is specified, it will
+    // only be removed if there is a match.
+    this.removeNextLine = function(regex) {
+        var nextnum = this.end.line + 1;
+        var removeit = false;
+        if ((nextnum < this.lines.length) && (regex) && (this.lines[nextnum].match(regex)))
+            removeit = true;
+        if ((nextnum < this.lines.length) && (!regex))
+            removeit = true;
+        if (removeit)
+            this.lines.splice(nextnum, 1);
+    };
+
+    // Remove the specified prefix from the selection
+    this.removePrefix = function(prefix) {
+        if (this.hasPrefix(prefix)) {
+            var line = this.lines[this.start.line];
+            var start = this.start.position - prefix.length;
+            var newline = line.substring(0, start) +
+                line.substring(this.start.position, line.length);
+            this.lines[this.start.line] = newline;
+            this.start.position -= prefix.length;
+            if (this.start.line == this.end.line)
+                this.end.position -= prefix.length;
+        }
+    };
+
+    // Remove the previous line. If regex is specified, it will
+    // only be removed if there is a match.
+    this.removePreviousLine = function(regex) {
+        var prevnum = this.start.line - 1;
+        var removeit = false;
+        if ((prevnum >= 0) && (regex) && (this.lines[prevnum].match(regex)))
+            removeit = true;
+        if ((prevnum >= 0) && (!regex))
+            removeit = true;
+        if (removeit) {
+            this.lines.splice(prevnum, 1);
+            this.start.line -= 1;
+            this.end.line -= 1;
+        }
+    };
+
+    // Remove the specified suffix from the selection
+    this.removeSuffix = function(suffix) {
+        if (this.hasSuffix(suffix)) {
+            var line = this.lines[this.end.line];
+            var end = this.end.position + suffix.length;
+            var newline = line.substring(0, this.end.position) +
+                line.substring(end, line.length);
+            this.lines[this.end.line] = newline;
+        }
+    };
+
+    // Set the prefix of each selected line. If the prefix is already and
+    // set, the line willl be left alone.
+    this.setLinePrefixes = function(prefix, increment) {
+        increment = increment ? increment : false;
+        for (var i=this.start.line; i <= this.end.line; i++) {
+            if (!this.lines[i].startswith(prefix)) {
+                // Check if prefix is incrementing
+                if (increment) {
+                    var num = parseInt(prefix.match(/\d+/)[0]);
+                    prefix = prefix.replace(num, num+1);
+                }
+                // Add the prefix to the line
+                var numspaces = this.lines[i].match(/^\s*/)[0].length;
+                this.lines[i] = this.lines[i].lstrip();
+                this.lines[i] = prefix + this.lines[i];
+                if (i == this.start.line)
+                    this.start.position += prefix.length - numspaces;
+                if (i == this.end.line)
+                    this.end.position += prefix.length - numspaces;
+            }
+        }
+    };
+
+    // Unwrap the selection prefix & suffix
+    this.unwrap = function(prefix, suffix) {
+        this.removePrefix(prefix);
+        this.removeSuffix(suffix);
+    };
+
+    // Remove blank lines from before and after the selection.  If the
+    // previous or next line is not blank, it will be left alone.
+    this.unwrapBlankLines = function() {
+        wysiwym.selection.removePreviousLine(/^\s*$/);
+        wysiwym.selection.removeNextLine(/^\s*$/);
+    };
+
+    // Return the selection value
+    this.val = function() {
+        var value = '';
+        for (var i=0; i < this.lines.length; i++) {
+            var line = this.lines[i];
+            if ((i == this.start.line) && (i == this.end.line)) {
+                return line.substring(this.start.position, this.end.position);
+            } else if (i == this.start.line) {
+                value += line.substring(this.start.position, line.length) +'\n';
+            } else if ((i > this.start.line) && (i < this.end.line)) {
+                value += line +'\n';
+            } else if (i == this.end.line) {
+                value += line.substring(0, this.end.position)
+            }
+        }
+        return value;
+    };
+
+    // Wrap the selection with the specified prefix & suffix
+    this.wrap = function(prefix, suffix) {
+        this.addPrefix(prefix);
+        this.addSuffix(suffix);
+    };
+
+    // Wrap the selected lines with blank lines.  If there is already
+    // a blank line in place, another one will not be added.
+    this.wrapBlankLines = function() {
+        if (wysiwym.selection.start.line > 0)
+            wysiwym.selection.insertPreviousLine(BLANKLINE, false);
+        if (wysiwym.selection.end.line < wysiwym.lines.length - 1)
+            wysiwym.selection.insertNextLine(BLANKLINE, false);
+    };
+
+}
+
+
+/*----------------------------------------------------------------------------------------------
+ * Wysiwym Textarea
+ * This can used used for some or all of your textarea modifications. It will keep track of
+ * the the current text and selection positions. The general idea is to keep track of the
+ * textarea in terms of Line objects.  A line object contains a lineType and supporting text.
+ *--------------------------------------------------------------------------------------------- */
+Wysiwym.Textarea = function(textarea) {
+    this.textelem = textarea.get(0)                 // Javascript textarea element
+    this.textarea = textarea;                       // jQuery textarea object
+    this.lines = [];                                // Current textarea lines
+    this.selection = new Wysiwym.Selection(this);   // Selection properties & manipulation
+    this.scroll = this.textelem.scrollTop;          // Current cursor scroll position
+
+    // Return a string representation of this object.
+    this.toString = function() {
+        var str = 'TEXTAREA: #'+ this.textarea.attr('id') +'\n';
+        str += this.selection.toString();
+        str += 'SCROLL: '+ this.scroll +'px\n';
+        str += '---\n';
+        for (var i=0; i<this.lines.length; i++)
+            str += 'LINE '+ i +': '+ this.lines[i] +'\n';
+        return str;
+    };
+
+    // Return the current text value of this textarea object
+    this.getProperties = function() {
+        var newtext = '';           // New textarea value
+        var selectionStart = 0;     // Absolute cursor start position
+        var selectionEnd = 0;       // Absolute cursor end position
+        for (var i=0; i < this.lines.length; i++) {
+            if (i == this.selection.start.line)
+                selectionStart = newtext.length + this.selection.start.position;
+            if (i == this.selection.end.line)
+                selectionEnd = newtext.length + this.selection.end.position;
+            newtext += this.lines[i];
+            if (i != this.lines.length - 1)
+                newtext += '\n';
+        }
+        return [newtext, selectionStart, selectionEnd];
+    };
+
+    // Return the absolute start and end selection postions
+    // StackOverflow #1: http://goo.gl/2vSnF
+    // StackOverflow #2: http://goo.gl/KHm0d
+    this.getSelectionStartEnd = function() {
+        if (typeof(this.textelem.selectionStart) == 'number') {
+            var startpos = this.textelem.selectionStart;
+            var endpos = this.textelem.selectionEnd;
+        } else {
+            this.textelem.focus();
+            var text = this.textelem.value.replace(/\r\n/g, '\n');
+            var textlen = text.length;
+            var range = document.selection.createRange();
+            var textrange = this.textelem.createTextRange();
+            textrange.moveToBookmark(range.getBookmark());
+            var endrange = this.textelem.createTextRange();
+            endrange.collapse(false);
+            if (textrange.compareEndPoints('StartToEnd', endrange) > -1) {
+                var startpos = textlen;
+                var endpos = textlen;
+            } else {
+                var startpos = -textrange.moveStart('character', -textlen);
+                //startpos += text.slice(0, startpos).split('\n').length - 1;
+                if (textrange.compareEndPoints('EndToEnd', endrange) > -1) {
+                    var endpos = textlen;
+                } else {
+                    var endpos = -textrange.moveEnd('character', -textlen);
+                    //endpos += text.slice(0, endpos).split('\n').length - 1;
+                }
+            }
+        }
+        return [startpos, endpos];
+    };
+
+    // Update the textarea with the current lines and cursor settings
+    this.update = function() {
+        var properties = this.getProperties();
+        var newtext = properties[0];
+        var selectionStart = properties[1];
+        var selectionEnd = properties[2];
+        this.textarea.val(newtext);
+        if (this.textelem.setSelectionRange) {
+            this.textelem.setSelectionRange(selectionStart, selectionEnd);
+        } else if (this.textelem.createTextRange) {
+            var range = this.textelem.createTextRange();
+            range.collapse(true);
+            range.moveStart('character', selectionStart);
+            range.moveEnd('character', selectionEnd - selectionStart);
+            range.select();
+        }
+        this.textelem.scrollTop = this.selection.scroll;
+        this.textarea.focus();
+    };
+
+    // Initialize the Wysiwym.Textarea
+    this.init = function() {
+        var text = textarea.val().replace(/\r\n/g, '\n');
+        var selectionInfo = this.getSelectionStartEnd(this.textelem);
+        var selectionStart = selectionInfo[0];
+        var selectionEnd = selectionInfo[1];
+        var endline = 0;
+        while (endline >= 0) {
+            var endline = text.indexOf('\n');
+            var line = text.substring(0, endline >= 0 ? endline : text.length);
+            if ((selectionStart <= line.length) && (selectionEnd >= 0)) {
+                if (selectionStart >= 0) {
+                    this.selection.start.line = this.lines.length;
+                    this.selection.start.position = selectionStart;
+                }
+                if (selectionEnd <= line.length) {
+                    this.selection.end.line = this.lines.length;
+                    this.selection.end.position = selectionEnd;
+                }
+            }
+            this.lines[this.lines.length] = line;
+            text = endline >= 0 ? text.substring(endline + 1, text.length) : '';
+            selectionStart -= endline + 1;
+            selectionEnd -= endline + 1;
+        }
+        // Tweak the selection end position if its on the edge
+        if ((this.selection.end.position == 0) && (this.selection.end.line != this.selection.start.line)) {
+            this.selection.end.line -= 1;
+            this.selection.end.position = this.lines[this.selection.end.line].length;
+        }
+    };
+    this.init();
 };
 
 
 /*----------------------------------------------------------------------------------------------
- * Textarea Object
- *   This can used used for some or all of your textarea modifications. It will keep track of
- *   the the current text and cursor positions. The general idea is to keep track of the
- *   textarea in terms of Line objects.  A line object contains a lineType and supporting text.
+ * Wysiwym Button
+ * Represents a single button in the Wysiwym editor.
  *--------------------------------------------------------------------------------------------- */
-function WysiwymTextarea(textarea) {
-    this.NEWLINE = "\n";                  // Newline Character specified to Browser
-    var NONE = { prefix: '' }             // Line Type NONE
-    var lines = new Array();              // Array Line objects { type, text, selected }
-    var cursor = {                        // Internal Cursor Object { start, end }
-        start: { line:0, position:0 },    // Cursor Start Position { line, position }
-        end: { line:0, position:0 },      // Cursor End Position { line, position }
-        scroll: 0                         // Current Scroll Position
+Wysiwym.Button = function(name, callback, data, cssclass) {
+    this.name = name;                  // Button Name
+    this.callback = callback;          // Callback function for this button
+    this.data = data ? data : {};      // Callback arguments
+    this.cssclass = cssclass;          // CSS Class to apply to button
+
+    // Return the CSS Class for this button
+    this.getCssClass = function() {
+        if (!this.cssclass)
+            return this.name.toLowerCase().replace(' ', '');
+        return this.cssclass;
+    };
+
+    // Create and return a new Button jQuery element
+    this.create = function() {
+        var text = $('<span class="text">'+ this.name +'</span>');
+        var wrap = $('<span class="wrap"></span>').append(text);
+        var button = $('<div class="button"></div>').append(wrap);
+        // Apply the title, css, and click bind.
+        button.attr('title', this.name);
+        button.addClass(this.getCssClass());
+        // Make everything 'unselectable' so IE doesn't freak out
+        text.attr('unselectable', 'on');
+        wrap.attr('unselectable', 'on');
+        button.attr('unselectable', 'on');
+        return button;
+    };
+}
+
+
+/*----------------------------------------------------------------------------------------------
+ * Wysiwym Button Callbacks
+ * Useful functions to help easily create Wysiwym buttons
+ *--------------------------------------------------------------------------------------------- */
+// Wrap the selected text with a prefix and suffix string.
+Wysiwym.span = function(event) {
+    var markup = event.data.markup;    // (required) Markup Language
+    var prefix = event.data.prefix;    // (required) Text wrap prefix
+    var suffix = event.data.suffix;    // (required) Text wrap suffix
+    var text = event.data.text;        // (required) Default wrap text (if nothing selected)
+    var wysiwym = new Wysiwym.Textarea(markup.textarea);
+    if (wysiwym.selection.isWrapped(prefix, suffix)) {
+        wysiwym.selection.unwrap(prefix, suffix);
+    } else if (wysiwym.selection.length() == 0) {
+        wysiwym.selection.append(text);
+        wysiwym.selection.wrap(prefix, suffix);
+    } else {
+        wysiwym.selection.wrap(prefix, suffix);
     }
+    wysiwym.update();
+};
 
-    /*-------------------------------------------------------------------
-     * Simple Public Functions
-     *------------------------------------------------------------------ */
-    this.getCursor = function()           { return cursor; }
-    this.getLines = function()            { return lines; }
-    this.getNumLines = function()         { return lines.length; }
-    this.getLine = function(lineNum)      { return lines[lineNum]; }
-    this.getSelectionRange = function()   { return [cursor.start.line, cursor.end.line]; }
+// Prefix each line in the selection with the specified text.
+Wysiwym.list = function(event) {
+    var markup = event.data.markup;    // (required) Markup Language
+    var prefix = event.data.prefix;    // (required) Line prefix text
+    var wrap = event.data.wrap;        // (optional) If true, wrap list with blank lines
+    var regex = event.data.regex;      // (optional) Set to regex matching prefix to increment num
+    var wysiwym = new Wysiwym.Textarea(markup.textarea);
+    if (wysiwym.selection.linesHavePrefix(regex?regex:prefix)) {
+        wysiwym.selection.removeLinePrefixes(regex?regex:prefix);
+        if (wrap) { wysiwym.selection.unwrapBlankLines(); }
+    } else {
+        wysiwym.selection.setLinePrefixes(prefix, regex);
+        if (wrap) { wysiwym.selection.wrapBlankLines(); }
+    }
+    wysiwym.update();
+};
 
-    /*-------------------------------------------------------------------
-     * @public getSelection: Return the current selected text.
-     *------------------------------------------------------------------ */
-    this.getSelection = function() {
-        var selection = "";
-        for (i in lines) {
-            var text = lines[i].text;
-            if ((i == cursor.start.line) && (i == cursor.end.line))
-                return text.substring(cursor.start.position, cursor.end.position);
-            if (i == cursor.start.line)
-                selection = selection + text.substring(cursor.start.position, text.length);
-            if ((i > cursor.start.line) && (i < cursor.end.line))
-                selection = selection + text;
-            if (i == cursor.end.line)
-                selection = selection + text.substring(0, cursor.end.position)
+// Prefix each line in the selection according based off the first selected line.
+Wysiwym.block = function(event) {
+    var markup = event.data.markup;    // (required) Markup Language
+    var prefix = event.data.prefix;    // (required) Line prefix text
+    var wrap = event.data.wrap;        // (optional) If true, wrap list with blank lines
+    var wysiwym = new Wysiwym.Textarea(markup.textarea);
+    var firstline = wysiwym.selection.getLines()[0];
+    if (firstline.startswith(prefix)) {
+        wysiwym.selection.removeLinePrefixes(prefix);
+        if (wrap) { wysiwym.selection.unwrapBlankLines(); }
+    } else {
+        wysiwym.selection.addLinePrefixes(prefix);
+        if (wrap) { wysiwym.selection.wrapBlankLines(); }
+    }
+    wysiwym.update();
+};
+
+
+/*----------------------------------------------------------------------------------------------
+ * Wysiwym AutoIndent
+ * Handles auto-indentation when enter is pressed
+ *--------------------------------------------------------------------------------------------- */
+Wysiwym.autoIndent = function(event) {
+    // Only continue if keyCode == 13
+    if (event.keyCode != 13)
+        return true;
+    // ReturnKey pressed, lets indent!
+    var markup = event.data.markup;    // Markup Language
+    var wysiwym = new Wysiwym.Textarea(markup.textarea);
+    var linenum = wysiwym.selection.start.line;
+    var line = wysiwym.lines[linenum];
+    var postcursor = line.substring(wysiwym.selection.start.position, line.length);
+    // Make sure nothing is selected & there is no text after the cursor
+    if ((wysiwym.selection.length() != 0) || (postcursor))
+        return true;
+    // So far so good; check for a matching indent regex
+    for (var i=0; i < markup.autoindents.length; i++) {
+        var regex = markup.autoindents[i];
+        var matches = line.match(regex);
+        if (matches) {
+            var prefix = matches[0];
+            var suffix = line.substring(prefix.length, line.length);
+            // NOTE: If a selection is made in the regex, it's assumed that the
+            // matching text is a number should be auto-incremented (ie: #. lists).
+            if (matches.length == 2) {
+                var num = parseInt(matches[1]);
+                prefix = prefix.replace(matches[1], num+1);
+            }
+            if (suffix) {
+                // Regular auto-indent; Repeat the prefix
+                wysiwym.selection.addPrefix('\n'+ prefix);
+                wysiwym.update();
+                return false;
+            } else {
+                // Return on blank indented line (clear prefix)
+                wysiwym.lines[linenum] = BLANKLINE;
+                wysiwym.selection.start.position = 0;
+                wysiwym.selection.end.position = wysiwym.selection.start.position;
+                if (markup.exitindentblankline) {
+                    wysiwym.selection.addPrefix('\n');
+                }
+                wysiwym.update();
+                return false;
+            }
         }
-        return selection;
     }
+    return true;
+}
 
-    /*-------------------------------------------------------------------
-     * @public getSelection: Return the length of Selection text
-     *------------------------------------------------------------------ */
-    this.getSelectionLength = function() {
-        var selection = this.getSelection();
-        return selection.length;
-    }
 
-    /*-------------------------------------------------------------------
-     * @public selectionHasPrefix: Return true if the current selection
-     *   has the specified prefix text. NOTE: Does not work with return lines.
-     *------------------------------------------------------------------ */
-    this.selectionHasPrefix = function(prefixText) {
-        var lineText = lines[cursor.start.line].text;
-        var start = cursor.start.position - prefixText.length;
-        if (start < 0) { return false; }
-        if (lineText.substring(start, cursor.start.position) != prefixText) { return false; }
-        return true;
-    }
+/* ---------------------------------------------------------------------------
+ * Wysiwym Markdown
+ * Markdown markup language for the Wysiwym editor
+ * Reference: http://daringfireball.net/projects/markdown/syntax
+ *---------------------------------------------------------------------------- */
+Wysiwym.Markdown = function(textarea) {
+    this.textarea = textarea;    // jQuery textarea object
 
-    /*-------------------------------------------------------------------
-     * @public selectionHasSuffix: Return true if the current selection
-     *   has the specified suffix text. NOTE: Does not work with return lines.
-     *------------------------------------------------------------------ */
-    this.selectionHasSuffix = function(suffixText) {
-        var lineText = lines[cursor.end.line].text;
-        var end = cursor.end.position + suffixText.length;
-        if (end > lineText.length) { return false; }
-        if (lineText.substring(cursor.end.position, end) != suffixText) { return false; }
-        return true;
-    }
+    // Initialize the Markdown Buttons
+    this.buttons = [
+        new Wysiwym.Button('Bold',   Wysiwym.span,  {prefix:'**', suffix:'**', text:'strong text'}),
+        new Wysiwym.Button('Italic', Wysiwym.span,  {prefix:'_',  suffix:'_',  text:'italic text'}),
+        new Wysiwym.Button('Link',   Wysiwym.span,  {prefix:'[',  suffix:'](http://example.com)', text:'link text'}),
+        new Wysiwym.Button('Bullet List', Wysiwym.list, {prefix:'* ', wrap:true}),
+        new Wysiwym.Button('Number List', Wysiwym.list, {prefix:'0. ', wrap:true, regex:/^\s*\d+\.\s/}),
+        new Wysiwym.Button('Quote',  Wysiwym.list,  {prefix:'> ',   wrap:true}),
+        new Wysiwym.Button('Code',   Wysiwym.block, {prefix:'    ', wrap:true})
+    ];
 
-    /*-------------------------------------------------------------------
-     * @public prependToSelection: Insert the specified text at at the cursor
-     *   start position and make it selected.
-     *------------------------------------------------------------------ */
-    this.prependToSelection = function(insertText) {
-        var lineText = lines[cursor.start.line].text;
-        var newText = lineText.substring(0, cursor.start.position) +
-            insertText + lineText.substring(cursor.start.position, lineText.length);
-        lines[cursor.start.line].text = newText;
-        // Update Class Variables
-        if (cursor.start.line == cursor.end.line)
-            cursor.end.position += insertText.length;
-    }
+    // Configure auto-indenting
+    this.exitindentblankline = true;    // True to insert blank line when exiting auto-indent ;)
+    this.autoindents = [                // Regex lookups for auto-indent
+        /^\s*\*\s/,                     // Bullet list
+        /^\s*(\d+)\.\s/,                // Number list with number (selected) for auto-increment
+        /^\s*\>\s/,                     // Quote list
+        /^\s{4}\s*/                     // Code block
+    ];
 
-    /*-------------------------------------------------------------------
-     * @public appendToSelection: Insert the specified text at at the cursor
-     *   end position and make it selected.
-     *------------------------------------------------------------------ */
-    this.appendToSelection = function(insertText) {
-        var lineText = lines[cursor.end.line].text;
-        var newText = lineText.substring(0, cursor.end.position) +
-            insertText + lineText.substring(cursor.end.position, lineText.length);
-        lines[cursor.end.line].text = newText;
-        // Update Class Variables
-        cursor.end.position += insertText.length;
-    }
+    // Syntax items to display in the help box
+    this.help = [
+        { label: 'Header', syntax: '## Header ##' },
+        { label: 'Bold',   syntax: '**bold**' },
+        { label: 'Italic', syntax: '_italics_' },
+        { label: 'Link',   syntax: '[pk!](http://google.com)' },
+        { label: 'Bullet List', syntax: '* list item' },
+        { label: 'Number List', syntax: '1. list item' },
+        { label: 'Blockquote', syntax: '&gt; quoted text' },
+        { label: 'Large Code Block', syntax: '(Begin lines with 4 spaces)' },
+        { label: 'Inline Code Block', syntax: '&lt;code&gt;inline code&lt;/code&gt;' }
+    ];
+};
 
-    /*-------------------------------------------------------------------
-     * @public insertSelectionPrefix: Insert the specified text at at the cursor
-     *   start position.  NOTE: This does not work with return lines.
-     *------------------------------------------------------------------ */
-    this.insertSelectionPrefix = function(prefixText) {
-        var lineText = lines[cursor.start.line].text;
-        var newText = lineText.substring(0, cursor.start.position) +
-            prefixText + lineText.substring(cursor.start.position, lineText.length);
-        lines[cursor.start.line].text = newText;
-        // Update Class Variables
-        cursor.start.position += prefixText.length;
-        if (cursor.start.line == cursor.end.line)
-            cursor.end.position += prefixText.length;
-    }
 
-    /*-------------------------------------------------------------------
-     * @public insertSelectionSuffix: Insert the specified text at at the cursor
-     *   end position.  NOTE: This does not work with return lines.
-     *------------------------------------------------------------------ */
-    this.insertSelectionSuffix = function(suffixText) {
-        var lineText = lines[cursor.end.line].text;
-        var newText = lineText.substring(0, cursor.end.position) +
-            suffixText + lineText.substring(cursor.end.position, lineText.length);
-        lines[cursor.end.line].text = newText;
-    }
+/* ---------------------------------------------------------------------------
+ * Wysiwym Mediawiki
+ * Media Wiki markup language for the Wysiwym editor
+ * Reference: http://www.mediawiki.org/wiki/Help:Formatting
+ *---------------------------------------------------------------------------- */
+Wysiwym.Mediawiki = function(textarea) {
+    this.textarea = textarea;    // jQuery textarea object
 
-    /*-------------------------------------------------------------------
-     * @public removePrefix: Remove the specified prefix text if it
-     *   matches with the passed in value.  Returns boolean True it
-     *   was successful, False otherwise.
-     *------------------------------------------------------------------ */
-    this.removeSelectionPrefix = function(prefixText) {
-        if (this.selectionHasPrefix(prefixText)) {
-            var lineText = lines[cursor.start.line].text;
-            var start = cursor.start.position - prefixText.length;
-            var newText = lineText.substring(0, start) +
-                lineText.substring(cursor.start.position, lineText.length);
-            lines[cursor.start.line].text = newText;
-            // Update Class Variables
-            cursor.start.position -= prefixText.length;
-            if (cursor.start.line == cursor.end.line)
-                cursor.end.position -= prefixText.length;
-            return true;
-        }
-        return false;
-    }
+    // Initialize the Markdown Buttons
+    this.buttons = [
+        new Wysiwym.Button('Bold',   Wysiwym.span,  {prefix:"'''", suffix:"'''", text:'strong text'}),
+        new Wysiwym.Button('Italic', Wysiwym.span,  {prefix:"''",  suffix:"''",  text:'italic text'}),
+        new Wysiwym.Button('Link',   Wysiwym.span,  {prefix:'[http://example.com ',  suffix:']', text:'link text'}),
+        new Wysiwym.Button('Bullet List', Wysiwym.list, {prefix:'* ', wrap:true}),
+        new Wysiwym.Button('Number List', Wysiwym.list, {prefix:'# ', wrap:true}),
+        new Wysiwym.Button('Quote',  Wysiwym.span,  {prefix:'<blockquote>', suffix:'</blockquote>', text:'quote text'}),
+        new Wysiwym.Button('Code', Wysiwym.span,  {prefix:'<pre>', suffix:'</pre>', text:'code text'}),
+    ];
 
-    /*-------------------------------------------------------------------
-     * @public removeSuffix: Remove the specified suffix text if it
-     *   matches with the passed in value.  Returns boolean True if it
-     *   was successful, False otherwise.
-     *------------------------------------------------------------------ */
-    this.removeSelectionSuffix = function(suffixText) {
-        if (this.selectionHasSuffix(suffixText)) {
-            var lineText = lines[cursor.end.line].text;
-            var end = cursor.end.position + suffixText.length;
-            var newText = lineText.substring(0, cursor.end.position) +
-                lineText.substring(end, lineText.length);
-            lines[cursor.end.line].text = newText;
-            return true;
-        }
-        return false;
-    }
+    // Configure auto-indenting
+    this.exitindentblankline = true;    // True to insert blank line when exiting auto-indent ;)
+    this.autoindents = [                // Regex lookups for auto-indent
+        /^\s*\*\s/,                     // Bullet list
+        /^\s*\#\s/,                     // Number list
+    ];
 
-    /*-------------------------------------------------------------------
-     * @public isWrapped: Return True if the selection is wrapped
-     *   in the specified prefix and suffix text.  Otherwise False.
-     *   NOTE: Does not work with return lines.
-     *------------------------------------------------------------------ */
-    this.selectionIsWrapped = function(prefixText, suffixText) {
-        if (suffixText == null) { suffixText = prefixText; }
-        return ((this.selectionHasPrefix(prefixText)) && (this.selectionHasSuffix(suffixText)))
-    }
+    // Syntax items to display in the help box
+    this.help = [
+        { label: 'Header', syntax: '== Header ==' },
+        { label: 'Bold',   syntax: "'''bold'''" },
+        { label: 'Italic', syntax: "''italics''" },
+        { label: 'Link',   syntax: '[http://google.com pk!]' },
+        { label: 'Bullet List', syntax: '* list item' },
+        { label: 'Number List', syntax: '# list item' },
+        { label: 'Blockquote', syntax: '&lt;blockquote&gt;quote&lt;/blockquote&gt;' },
+        { label: 'Large Code Block', syntax: '&lt;pre&gt;Code block&lt;/pre&gt;' }
+    ];
+};
 
-    /*-------------------------------------------------------------------
-     * @public wrap: Wrap the cursor or currently selected text
-     *   with the specified newTextBefore and newTextAfter.
-     *------------------------------------------------------------------ */
-    this.wrapSelection = function(prefixText, suffixText) {
-        if (suffixText == null) { suffixText = prefixText; }
-        this.insertSelectionPrefix(prefixText);
-        this.insertSelectionSuffix(suffixText);
-    }
 
-    /*-------------------------------------------------------------------
-     * @public unwrap: Unwrap the selection by removeing the prefix
-     *   and suffix text specified.  Returns boolean True it
-     *   was successful, False otherwise.
-     *------------------------------------------------------------------ */
-    this.unwrapSelection = function(prefixText, suffixText) {
-        if (suffixText == null) { suffixText = prefixText; }
-        if ((this.selectionHasPrefix(prefixText)) && (this.selectionHasSuffix(suffixText))) {
-            this.removeSelectionPrefix(prefixText);
-            this.removeSelectionSuffix(suffixText);
-            return true;
-        }
-        return false;
-    }
+/* ---------------------------------------------------------------------------
+ * Wysiwym BBCode
+ * BBCode markup language for the Wysiwym editor
+ * Reference: http://labs.spaceshipnofuture.org/icky/help/formatting/
+ *---------------------------------------------------------------------------- */
+Wysiwym.BBCode = function(textarea) {
+    this.textarea = textarea;    // jQuery textarea object
 
-    /*-------------------------------------------------------------------
-     * @public insertLinePrefix: Return True if the line has the
-     *   specified prefix.
-     *------------------------------------------------------------------ */
-    this.lineHasPrefix = function(lineNum, prefixText) {
-        return lines[lineNum].text.substring(0, prefixText.length) == prefixText;
-    }
+    // Initialize the Markdown Buttons
+    this.buttons = [
+        new Wysiwym.Button('Bold',   Wysiwym.span,  {prefix:"[b]", suffix:"[/b]", text:'strong text'}),
+        new Wysiwym.Button('Italic', Wysiwym.span,  {prefix:"[i]",  suffix:"[/i]",  text:'italic text'}),
+        new Wysiwym.Button('Link',   Wysiwym.span,  {prefix:'[url="http://example.com"]',  suffix:'[/url]', text:'link text'}),
+        new Wysiwym.Button('Quote',  Wysiwym.span,  {prefix:'[quote]',  suffix:'[/quote]', text:'quote text'}),
+        new Wysiwym.Button('Code',   Wysiwym.span,  {prefix:'[code]',  suffix:'[/code]', text:'code text'}),
+    ];
 
-    /*-------------------------------------------------------------------
-     * @public insertLinePrefix: Add the Prefix to the specified line.
-     *------------------------------------------------------------------ */
-    this.insertLinePrefix = function(lineNum, prefixText) {
-        var lineText = lines[lineNum].text;
-        var newText = prefixText + lineText.substring(0, cursor.start.position) +
-            lineText.substring(cursor.start.position, lineText.length);
-        lines[lineNum].text = newText;
-        // Update Class Variables
-        if (lineNum == cursor.start.line)
-            cursor.start.position += prefixText.length;
-        if (lineNum == cursor.end.line)
-            cursor.end.position += prefixText.length;
-    }
+    // Syntax items to display in the help box
+    this.help = [
+        { label: 'Bold',   syntax: "[b]bold[/b]" },
+        { label: 'Italic', syntax: "[i]italics[/i]" },
+        { label: 'Link',   syntax: '[url="http://example.com"]pk![/url]' },
+        { label: 'Blockquote', syntax: '[quote]quote text[/quote]' },
+        { label: 'Large Code Block', syntax: '[code]code text[/code]' }
+    ];
+};
 
-    /*-------------------------------------------------------------------
-     * @public removePrefix: Remove the specified prefix text if it
-     *   matches with the passed in value.  Returns boolean True it
-     *   was successful, False otherwise.
-     *------------------------------------------------------------------ */
-    this.removeLinePrefix = function(lineNum, prefixText) {
-        if (this.lineHasPrefix(lineNum, prefixText)) {
-            var lineText = lines[lineNum].text;
-            var newText = lineText.substring(prefixText.length, lineText.length);
-            lines[lineNum].text = newText;
-            // Update Class Variables
-            if (lineNum == cursor.start.line)
-                cursor.start.position -= prefixText.length;
-            if (lineNum == cursor.end.line)
-                cursor.end.position -= prefixText.length;
-            return true;
-        }
-        return false;
-    }
-
-    /*-------------------------------------------------------------------
-     * @public insertLineSuffix: Append the suffix to the end of the
-     *   specified line.
-     *------------------------------------------------------------------ */
-    this.insertLineSuffix = function(lineNum, suffixText) {
-        var lineText = lines[lineNum].text;
-        var newText = lineText.substring(0, cursor.start.position) +
-            lineText.substring(cursor.start.position, lineText.length) + suffixText;
-        lines[lineNum].text = newText;
-    }
-
-    /*-------------------------------------------------------------------
-     * @public insertLinePrefix: Return True if all lines in the
-     *   selection have the specified prefix.
-     *------------------------------------------------------------------ */
-    this.selectionLinesHavePrefix = function(prefixText) {
-        for (var i=cursor.start.line; i<=cursor.end.line; i++)
-            if (!this.lineHasPrefix(i, prefixText))
-                return false;
-        return true;
-    }
-
-    /*-------------------------------------------------------------------
-     * @public insertLinePrefix: Add the Prefix Text to all lines in
-     *   the selection.
-     *------------------------------------------------------------------ */
-    this.insertSelectionLinesPrefix = function(prefixText) {
-        for (var i=cursor.start.line; i<=cursor.end.line; i++)
-            this.insertLinePrefix(i, prefixText);
-    }
-
-    /*-------------------------------------------------------------------
-     * @public insertLinePrefix: Add the Prefix Text to all lines in
-     *   the selection.
-     *------------------------------------------------------------------ */
-    this.removeSelectionLinesPrefix = function(prefixText) {
-        for (var i=cursor.start.line; i<=cursor.end.line; i++)
-            this.removeLinePrefix(i, prefixText);
-    }
-
-    /*-------------------------------------------------------------------
-     * @public getTextareaProperties: Return the global textarea
-     *   properties (NOT by Line).  This returns the a Map containing:
-     *   { text, cursorStart, cursorEnd, selectionLength, scroll }
-     *------------------------------------------------------------------ */
-    this.getTextareaProperties = function() {
-        var text = ""               // Global Textarea Value
-        var cursorStart = 0;        // Cursor Start Position (chars from beginning of textarea)
-        var cursorEnd = 0;          // Cursor End Position (chars from beginning of textarea)
-        var selectionLength = 0;    // Selection Length (total chars)
-        var lineStart = 0;
-	for (var i=0; i<lines.length; i++) {
-            text += lines[i].text;
-            if (i == cursor.start.line)
-                cursorStart = lineStart + cursor.start.position;
-            if (i == cursor.end.line)
-                cursorEnd = lineStart + cursor.end.position;
-            lineStart += lines[i].text.length;
-        }
-        var selectionLength = cursorEnd - cursorStart;
-        return { text:text, cursorStart:cursorStart, cursorEnd:cursorEnd,
-            selectionLength:selectionLength, scroll:cursor.scroll }
-    }
-
-    /*-------------------------------------------------------------------
-     * @public update: Update the HTML textarea to reflect all changes
-     *   made within this class.  This is generally the last step in
-     *   defining a markup button.
-     *------------------------------------------------------------------ */
-    this.update = function() {
-        var textProperties = this.getTextareaProperties();
-        // Update the Textarea
-        $(textarea).val(textProperties.text);
-        if (textarea.createTextRange) {
-            range = textarea.createTextRange();
-            range.collapse(true);
-            range.moveStart('character', textProperties.cursorStart);
-            range.moveEnd('character', textProperties.selectionLength);
-            range.select();
-        } else if (textarea.setSelectionRange) {
-            textarea.setSelectionRange(textProperties.cursorStart, textProperties.cursorEnd);
-        }
-        textarea.scrollTop = textProperties.scroll;
-        textarea.focus();
-    }
-
-    /*-------------------------------------------------------------------
-     * @private getCursorProperties: Returns a Map object containing the
-     *   current the current cursor properties for scroll position,
-     *   cursor position and selection length.
-     *------------------------------------------------------------------ */
-    function getCursorProperties() {
-        textarea.focus();
-        var scroll = textarea.scrollTop;   // Current Scroll Position
-        var position = 0;                  // Current Cursor Position
-        var selection = "";                // Current Selection Length
-        if (document.selection) {
-            // Internet Explorer
-            selection = document.selection.createRange().text;
-            if ($.browser.msie) {
-                var range = document.selection.createRange();
-                var rangeCopy = range.duplicate();
-                rangeCopy.moveToElementText(textarea);
-                position = -1;
-                while(rangeCopy.inRange(range)) {
-                    rangeCopy.moveStart('character');
-                    position++;
-                }
-            } else {
-                // Opera
-                position = textarea.selectionStart;
-            }
-        } else {
-            // Mozilla
-            position = textarea.selectionStart;
-            selection = $(textarea).val().substring(position, textarea.selectionEnd);
-        }
-        return {scroll: scroll, position: position, length: selection.length}
-    }
-
-    /*-------------------------------------------------------------------
-     * @private setupPropertiesByLine: Updates the class variables for
-     *   lines in the Textarea and cursor start and end position
-     *   (in terms of lines).
-     *------------------------------------------------------------------ */
-    function setupPropertiesByLine() {
-        var text = $(textarea).val();                              // Initial Textarea Value
-        var cursorInfo = getCursorProperties();                    // Cursor Properties
-        var cursorStart = cursorInfo.position;                     // Global Cursor Start Position
-        var cursorEnd = cursorInfo.position + cursorInfo.length;   // Global Cursor End Position
-        // Parse the Current Textarea
-        var num = 0;               // Iter Line Number
-        var lineStart = 0;         // Iter Position
-        var lastLine = false;      // Flag Set on Last Line
-        while (!lastLine) {
-            // Find the Line Ending Position
-            var lineEnd = text.indexOf("\n", lineStart);
-            var charEnd = lineEnd;
-            if (lineEnd >= 0) { lineEnd += 1; }
-            else { lastLine = true; lineEnd = charEnd = text.length; }
-            // Get the Line Properties
-            var lineText = text.substring(lineStart, lineEnd);
-            var selected = (cursorStart <= charEnd) && (cursorEnd >= lineStart)
-            lines[num] = { text:lineText, selected:selected };
-            // Update the Cursor Information
-            if ((cursorStart >= lineStart) && (cursorStart <= charEnd))
-                cursor.start = { line: num, position:cursorStart-lineStart };
-            if ((cursorEnd >= lineStart) && (cursorEnd <= lineEnd))
-                cursor.end = { line:num, position:cursorEnd-lineStart};
-            // Update Properties for Next Line
-            lineStart = lineEnd;
-            num += 1;
-        }
-        // Save the Scroll Position
-        cursor.scroll = cursorInfo.scroll;
-    }
-
-    /*--- Main ---*/
-    setupPropertiesByLine();
-
-}
 
 /*----------------------------------------------------------------------
  * Additional Javascript Prototypes
  *-------------------------------------------------------------------- */
-String.prototype.trim  = function() { return this.replace(/^\s+|\s+$/g,""); }
-String.prototype.ltrim = function() { return this.replace(/^\s+/,""); }
-String.prototype.rtrim = function() { return this.replace(/\s+$/,""); }
+String.prototype.strip = function() { return this.replace(/^\s+|\s+$/g, ''); };
+String.prototype.lstrip = function() { return this.replace(/^\s+/, ''); };
+String.prototype.rstrip = function() { return this.replace(/\s+$/, ''); };
+String.prototype.startswith = function(str) { return this.substring(0, str.length) == str; };
+String.prototype.endswith = function(str) { return this.substring(str.length, this.length) == str; };