Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added ul/ol support. Also, mocha testing.

Added support for UL/OL lists and children tags of [*] (just transpiled
inside for easier support).

Reworked the tests to use mocha.

Signed-off-by: Nick Campbell <nicholas.j.campbell@gmail.com>
  • Loading branch information...
commit 40e727ba3cbceee460cfd971d870077fcc7efbf8 1 parent 23ebd01
Nick Campbell authored
Showing with 219 additions and 156 deletions.
  1. +1 −2  Makefile
  2. +169 −154 lib/bbcode.js
  3. +3 −0  package.json
  4. +46 −0 tests/parse.js
3  Makefile
View
@@ -4,7 +4,6 @@ all: test
test:
npm install .
- @./node_modules/nodeunit/bin/nodeunit \
- $(TESTS)
+ @mocha $(TESTS)
.PHONY: test
323 lib/bbcode.js
View
@@ -38,229 +38,244 @@
// text containing [noparse] [brackets][/noparse]
//
// -----------------------------------------------------------------------
-var opentags = []; // open tag stack
-var crlf2br = true; // convert CRLF to <br>?
-var noparse = false; // ignore BBCode tags?
-
+//
+// post must be HTML-encoded
+//
+exports.parse = function(post, cb) {
+ var opentags = []; // open tag stack
+ var crlf2br = true; // convert CRLF to <br>?
+ var noparse = false; // ignore BBCode tags?
-var urlstart = -1; // beginning of the URL if zero or greater (ignored if -1)
+ var urlstart = -1; // beginning of the URL if zero or greater (ignored if -1)
-// aceptable BBcode tags, optionally prefixed with a slash
-var tagname_re = /^\/?(?:b|i|u|pre|samp|code|colou?r|size|noparse|url|s|q|blockquote|img)$/;
+ // aceptable BBcode tags, optionally prefixed with a slash
+ var tagname_re = /^\/?(?:b|i|u|pre|samp|code|colou?r|size|noparse|url|s|q|blockquote|img|u?list|li)$/;
-// color names or hex color
-var color_re = /^(:?black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua|#(?:[0-9a-f]{3})?[0-9a-f]{3})$/i;
+ // color names or hex color
+ var color_re = /^(:?black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua|#(?:[0-9a-f]{3})?[0-9a-f]{3})$/i;
-// numbers
-var number_re = /^[\\.0-9]{1,8}$/i;
+ // numbers
+ var number_re = /^[\\.0-9]{1,8}$/i;
-// reserved, unreserved, escaped and alpha-numeric [RFC2396]
-var uri_re = /^[-;\/\?:@&=\+\$,_\.!~\*'\(\)%0-9a-z]{1,512}$/i;
+ // reserved, unreserved, escaped and alpha-numeric [RFC2396]
+ var uri_re = /^[-;\/\?:@&=\+\$,_\.!~\*'\(\)%0-9a-z]{1,512}$/i;
-// main regular expression: CRLF, [tag=option], [tag] or [/tag]
-var postfmt_re = /([\r\n])|(?:\[([a-z]{1,16})(?:=([^\x00-\x1F"'\(\)<>\[\]]{1,256}))?\])|(?:\[\/([a-z]{1,16})\])/ig;
+ // main regular expression: CRLF, [tag=option], [tag] or [/tag]
+ var postfmt_re = /([\r\n])|(?:\[([a-z]{1,16})(?:=([^\x00-\x1F"'\(\)<>\[\]]{1,256}))?\])|(?:\[\/([a-z]{1,16})\])/ig;
-// stack frame object
-function taginfo_t(bbtag, etag)
-{
+ // stack frame object
+ function taginfo_t(bbtag, etag)
+ {
return {
- bbtag: bbtag,
+ bbtag: bbtag,
etag: etag
};
-}
+ }
-// check if it's a valid BBCode tag
-function isValidTag(str)
-{
+ // check if it's a valid BBCode tag
+ function isValidTag(str)
+ {
if(!str || !str.length)
- return false;
+ return false;
return tagname_re.test(str);
-}
-
-//
-// m1 - CR or LF
-// m2 - the tag of the [tag=option] expression
-// m3 - the option of the [tag=option] expression
-// m4 - the end tag of the [/tag] expression
-//
-function textToHtmlCB(mstr, m1, m2, m3, m4, offset, string)
-{
+ }
+
+ //
+ // m1 - CR or LF
+ // m2 - the tag of the [tag=option] expression
+ // m3 - the option of the [tag=option] expression
+ // m4 - the end tag of the [/tag] expression
+ //
+ function textToHtmlCB(mstr, m1, m2, m3, m4, offset, string)
+ {
//
// CR LF sequences
//
if(m1 && m1.length) {
- if(!crlf2br)
- return mstr;
+ if(!crlf2br)
+ return mstr;
- switch (m1) {
+ switch (m1) {
case '\r':
- return "";
+ return "";
case '\n':
- return "<br>";
- }
+ return "<br>";
+ }
}
//
// handle start tags
//
if(isValidTag(m2)) {
- // if in the noparse state, just echo the tag
- if(noparse)
- return "[" + m2 + "]";
+ // if in the noparse state, just echo the tag
+ if(noparse)
+ return "[" + m2 + "]";
- // ignore any tags if there's an open option-less [url] tag
- if(opentags.length && opentags[opentags.length-1].bbtag == "url" && urlstart >= 0)
- return "[" + m2 + "]";
+ // ignore any tags if there's an open option-less [url] tag
+ if(opentags.length && opentags[opentags.length-1].bbtag == "url" && urlstart >= 0)
+ return "[" + m2 + "]";
- switch (m2) {
+ switch (m2) {
case "code":
- opentags.push(new taginfo_t(m2, "</code></pre>"));
- crlf2br = false;
- return "<pre><code>";
+ opentags.push(new taginfo_t(m2, "</code></pre>"));
+ crlf2br = false;
+ return "<pre><code>";
case "pre":
- opentags.push(new taginfo_t(m2, "</pre>"));
- crlf2br = false;
- return "<pre>";
+ opentags.push(new taginfo_t(m2, "</pre>"));
+ crlf2br = false;
+ return "<pre>";
case "color":
case "colour":
- if(!m3 || !color_re.test(m3))
- m3 = "inherit";
- opentags.push(new taginfo_t(m2, "</span>"));
- return "<span style=\"color: " + m3 + "\">";
+ if(!m3 || !color_re.test(m3))
+ m3 = "inherit";
+ opentags.push(new taginfo_t(m2, "</span>"));
+ return "<span style=\"color: " + m3 + "\">";
case "size":
- if(!m3 || !number_re.test(m3))
- m3 = "1";
- opentags.push(new taginfo_t(m2, "</span>"));
- return "<span style=\"font-size: " + Math.min(Math.max(m3, 0.7), 3) + "em\">";
+ if(!m3 || !number_re.test(m3))
+ m3 = "1";
+ opentags.push(new taginfo_t(m2, "</span>"));
+ return "<span style=\"font-size: " + Math.min(Math.max(m3, 0.7), 3) + "em\">";
case "s":
- opentags.push(new taginfo_t(m2, "</span>"));
- return "<span style=\"text-decoration: line-through\">";
+ opentags.push(new taginfo_t(m2, "</span>"));
+ return "<span style=\"text-decoration: line-through\">";
case "noparse":
- noparse = true;
- return "";
+ noparse = true;
+ return "";
case "url":
- opentags.push(new taginfo_t(m2, "</a>"));
+ opentags.push(new taginfo_t(m2, "</a>"));
- // check if there's a valid option
- if(m3 && uri_re.test(m3)) {
- // if there is, output a complete start anchor tag
- urlstart = -1;
- return "<a href=\"" + m3 + "\">";
- }
+ // check if there's a valid option
+ if(m3 && uri_re.test(m3)) {
+ // if there is, output a complete start anchor tag
+ urlstart = -1;
+ return "<a href=\"" + m3 + "\">";
+ }
- // otherwise, remember the URL offset
- urlstart = mstr.length + offset;
+ // otherwise, remember the URL offset
+ urlstart = mstr.length + offset;
- // and treat the text following [url] as a URL
- return "<a href=\"";
- case "img":
- opentags.push(new taginfo_t(m2, "\" />"));
+ // and treat the text following [url] as a URL
+ return "<a href=\"";
+ case "img":
+ opentags.push(new taginfo_t(m2, "\" />"));
- if (m3 && uri_re.test(m3)) {
- urlstart = -1;
- return "<" + m2 + " src=\"" + m3 + "";
- }
+ if (m3 && uri_re.test(m3)) {
+ urlstart = -1;
+ return "<" + m2 + " src=\"" + m3 + "";
+ }
- return "<"+m2+" src=\"";
+ return "<"+m2+" src=\"";
case "q":
case "blockquote":
- opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
- return m3 && m3.length && uri_re.test(m3) ? "<" + m2 + " cite=\"" + m3 + "\">" : "<" + m2 + ">";
+ opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
+ return m3 && m3.length && uri_re.test(m3) ? "<" + m2 + " cite=\"" + m3 + "\">" : "<" + m2 + ">";
+
+ case "list":
+ opentags.push(new taginfo_t('list', '</ol>'));
+ return '<ol>';
+
+ case "ulist":
+ opentags.push(new taginfo_t('ulist', '</ul>'));
+ return '<ul>';
default:
- // [samp], [b], [i] and [u] don't need special processing
- opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
- return "<" + m2 + ">";
+ // [samp], [b], [i] and [u] don't need special processing
+ opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
+ return "<" + m2 + ">";
- }
+ }
}
//
// process end tags
//
if(isValidTag(m4)) {
- if(noparse) {
- // if it's the closing noparse tag, flip the noparse state
- if(m4 == "noparse") {
- noparse = false;
- return "";
- }
-
- // otherwise just output the original text
- return "[/" + m4 + "]";
+ if(noparse) {
+ // if it's the closing noparse tag, flip the noparse state
+ if(m4 == "noparse") {
+ noparse = false;
+ return "";
}
- // highlight mismatched end tags
- if(!opentags.length || opentags[opentags.length-1].bbtag != m4)
- return "<span style=\"color: red\">[/" + m4 + "]</span>";
+ // otherwise just output the original text
+ return "[/" + m4 + "]";
+ }
- if(m4 == "url") {
- // if there was no option, use the content of the [url] tag
- if(urlstart > 0)
- return "\">" + string.substr(urlstart, offset-urlstart) + opentags.pop().etag;
+ // highlight mismatched end tags
+ if(!opentags.length || opentags[opentags.length-1].bbtag != m4)
+ return "<span style=\"color: red\">[/" + m4 + "]</span>";
- // otherwise just close the tag
- return opentags.pop().etag;
- }
- else if(m4 == "code" || m4 == "pre")
- crlf2br = true;
+ if(m4 == "url") {
+ // if there was no option, use the content of the [url] tag
+ if(urlstart > 0)
+ return "\">" + string.substr(urlstart, offset-urlstart) + opentags.pop().etag;
- // other tags require no special processing, just output the end tag
+ // otherwise just close the tag
return opentags.pop().etag;
+ }
+ else if(m4 == "code" || m4 == "pre")
+ crlf2br = true;
+
+ // other tags require no special processing, just output the end tag
+ var end = opentags.pop().etag;
+ return end;
}
return mstr;
-}
-
-//
-// post must be HTML-encoded
-//
-exports.parse = function(post, cb) {
- var result = '', endtags, tag;
-
- // convert CRLF to <br> by default
- crlf2br = true;
-
- // create a new array for open tags
- if(opentags == null || opentags.length)
- opentags = new Array(0);
-
- // run the text through main regular expression matcher
- if (post) {
- result = post.replace(postfmt_re, textToHtmlCB);
-
- // reset noparse, if it was unbalanced
- if(noparse)
- noparse = false;
-
- // if there are any unbalanced tags, make sure to close them
- if(opentags.length) {
- endtags = new String();
-
- // if there's an open [url] at the top, close it
- if(opentags[opentags.length-1].bbtag == "url") {
- opentags.pop();
- endtags += "\">" + post.substr(urlstart, post.length-urlstart) + "</a>";
- }
-
- // close remaining open tags
- while(opentags.length)
- endtags += opentags.pop().etag;
- }
- }
- var ret = endtags ? result + endtags : result;
- if (cb) {
- cb(ret);
- } else {
- return ret;
+ }
+
+ // actual parsing can begin
+ var result = '', endtags, tag;
+
+ // convert CRLF to <br> by default
+ crlf2br = true;
+
+ // create a new array for open tags
+ if(opentags == null || opentags.length)
+ opentags = new Array(0);
+
+ // run the text through main regular expression matcher
+ if (post) {
+ // idea to replace single *'s from http://patorjk.com/bbcode-previewer/
+ post = (function(_post) {
+ return _post.replace(/(\[\*\])([^\[]*)/g, function(m0, m1, m2, offset, mstr) {
+ return '[li]' + m2 + '[/li]';
+ });
+ })(post);
+ result = post.replace(postfmt_re, textToHtmlCB);
+
+ // reset noparse, if it was unbalanced
+ if(noparse)
+ noparse = false;
+
+ // if there are any unbalanced tags, make sure to close them
+ if(opentags.length) {
+ endtags = new String();
+
+ // if there's an open [url] at the top, close it
+ if(opentags[opentags.length-1].bbtag == "url") {
+ opentags.pop();
+ endtags += "\">" + post.substr(urlstart, post.length-urlstart) + "</a>";
+ }
+
+ // close remaining open tags
+ while(opentags.length)
+ endtags += opentags.pop().etag;
}
+ }
+ var ret = endtags ? result + endtags : result;
+ if (cb) {
+ cb(ret);
+ } else {
+ return ret;
+ }
}
3  package.json
View
@@ -17,6 +17,9 @@
"url" : "http://github.com/ncb000gt/node-bbcode/issues"
},
"dependencies": {
+ "underscore": "*"
+ },
+ "devDependencies": {
"mocha": "1.3.2",
"should": "1.1.0"
}
46 tests/parse.js
View
@@ -94,5 +94,51 @@ describe('bcrypt', function() {
parse.should.equal('<b>test</b>');
});
});
+
+ describe('should parse [list] and [*] children to <ol> and <li> respectively -', function() {
+ it('no children', function() {
+ bbcode.parse('[list][/list]', function(parse) {
+ parse.should.equal('<ol></ol>');
+ });
+ });
+
+ it('one on a single line', function() {
+ bbcode.parse('[list][*]one[/list]', function(parse) {
+ parse.should.equal('<ol><li>one</li></ol>');
+ });
+ });
+
+ it('two on a single line', function() {
+ bbcode.parse('[list][*]one[*]two[/list]', function(parse) {
+ parse.should.equal('<ol><li>one</li><li>two</li></ol>');
+ });
+ });
+ });
+
+ describe('should parse [ulist] and [*] children to <ul> and <li> respectively -', function() {
+ it('no children', function() {
+ bbcode.parse('[ulist][/ulist]', function(parse) {
+ parse.should.equal('<ul></ul>');
+ });
+ });
+
+ it('one on a single line', function() {
+ bbcode.parse('[ulist][*]one[/ulist]', function(parse) {
+ parse.should.equal('<ul><li>one</li></ul>');
+ });
+ });
+
+ it('two on a single line', function() {
+ bbcode.parse('[ulist][*]one[*]two[/ulist]', function(parse) {
+ parse.should.equal('<ul><li>one</li><li>two</li></ul>');
+ });
+ });
+ });
+
+ it('should parse inner tags [b][i][u] to <b><i><u>', function() {
+ bbcode.parse('[b][i][u]Hai[/u][/i][/b]', function(parse) {
+ parse.should.equal('<b><i><u>Hai</u></i></b>');
+ });
+ });
});
});
Please sign in to comment.
Something went wrong with that request. Please try again.