1 // -----------------------------------------------------------------------
2 // Copyright (c) 2008, Stone Steps Inc.
4 // http://www.stonesteps.ca/legal/bsd-license/
6 // This is a BBCode parser written in JavaScript. The parser is intended
7 // to demonstrate how to parse text containing BBCode tags in one pass
8 // using regular expressions.
10 // The parser may be used as a backend component in ASP or in the browser,
11 // after the text containing BBCode tags has been served to the client.
13 // Following BBCode expressions are recognized:
18 // [s]strike-through[/s]
19 // [samp]sample[/samp]
21 // [color=red]red[/color]
22 // [color=#FF0000]red[/color]
23 // [size=1.2]1.2em[/size]
25 // [url]http://blogs.stonesteps.ca/showpost.asp?pid=33[/url]
26 // [url=http://blogs.stonesteps.ca/showpost.asp?pid=33][b]BBCode[/b] Parser[/url]
28 // [q=http://blogs.stonesteps.ca/showpost.asp?pid=33]inline quote[/q]
29 // [q]inline quote[/q]
30 // [blockquote=http://blogs.stonesteps.ca/showpost.asp?pid=33]block quote[/blockquote]
31 // [blockquote]block quote[/blockquote]
36 // print("done");[/code]
38 // text containing [noparse] [brackets][/noparse]
40 // -----------------------------------------------------------------------
41 var opentags; // open tag stack
42 var crlf2br = true; // convert CRLF to <br>?
43 var noparse = false; // ignore BBCode tags?
44 var urlstart = -1; // beginning of the URL if zero or greater (ignored if -1)
46 // aceptable BBcode tags, optionally prefixed with a slash
47 var tagname_re = /^\/?(?:b|br|i|u|pre|samp|code|colou?r|size|noparse|url|s|q|blockquote)$/;
49 // color names or hex color
50 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;
53 var number_re = /^[\\.0-9]{1,8}$/i;
55 // reserved, unreserved, escaped and alpha-numeric [RFC2396]
56 var uri_re = /^[-;\/\?:@&=\+\$,_\.!~\*'\(\)%0-9a-z]{1,512}$/i;
58 // main regular expression: CRLF, [tag=option], [tag] or [/tag]
59 var postfmt_re = /([\r\n])|(?:\[([a-z]{1,16})(?:=([^\x00-\x1F"'\(\)<>\[\]]{1,256}))?\])|(?:\[\/([a-z]{1,16})\])|(?:\[([a-z]{1,16})\/\])/ig;
62 function taginfo_t(bbtag, etag)
68 // check if it's a valid BBCode tag
69 function isValidTag(str)
71 if(!str || !str.length)
74 return tagname_re.test(str);
79 // m2 - the tag of the [tag=option] expression
80 // m3 - the option of the [tag=option] expression
81 // m4 - the end tag of the [/tag] expression
83 function textToHtmlCB(mstr, m1, m2, m3, m4, m5, offset, string)
101 // if in the noparse state, just echo the tag
103 return "[" + m5 + "/]";
115 // if in the noparse state, just echo the tag
117 return "[" + m2 + "]";
119 // ignore any tags if there's an open option-less [url] tag
120 if(opentags.length && opentags[opentags.length-1].bbtag == "url" && urlstart >= 0)
121 return "[" + m2 + "]";
125 opentags.push(new taginfo_t(m2, "</code></pre>"));
127 return "<pre><code>";
130 opentags.push(new taginfo_t(m2, "</pre>"));
136 if(!m3 || !color_re.test(m3))
138 opentags.push(new taginfo_t(m2, "</span>"));
139 return "<span style=\"color: " + m3 + "\">";
142 if(!m3 || !number_re.test(m3))
144 opentags.push(new taginfo_t(m2, "</span>"));
145 return "<span style=\"font-size: " + Math.min(Math.max(m3, 0.7), 3) + "em\">";
148 opentags.push(new taginfo_t(m2, "</span>"));
149 return "<span style=\"text-decoration: line-through\">";
156 opentags.push(new taginfo_t(m2, "</a>"));
158 // check if there's a valid option
159 if(m3 && uri_re.test(m3)) {
160 // if there is, output a complete start anchor tag
162 return "<a href=\"" + m3 + "\">";
165 // otherwise, remember the URL offset
166 urlstart = mstr.length + offset;
168 // and treat the text following [url] as a URL
173 opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
174 return m3 && m3.length && uri_re.test(m3) ? "<" + m2 + " cite=\"" + m3 + "\">" : "<" + m2 + ">";
177 opentags.push(new taginfo_t(m2, "</span>"));
178 return "<span style=\"font-weight: bold\">";
181 opentags.push(new taginfo_t(m2, "</span>"));
182 return "<span style=\"font-style:italic\">";
185 opentags.push(new taginfo_t(m2, "</span>"));
186 return "<span style=\"text-decoration: underline\">";
189 // [samp]don't need special processing
190 opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
191 return "<" + m2 + ">";
201 // if it's the closing noparse tag, flip the noparse state
202 if(m4 == "noparse") {
207 // otherwise just output the original text
208 return "[/" + m4 + "]";
211 // highlight mismatched end tags
212 if(!opentags.length || opentags[opentags.length-1].bbtag != m4)
213 return "<span style=\"color: red\">[/" + m4 + "]</span>";
216 // if there was no option, use the content of the [url] tag
218 return "\">" + string.substr(urlstart, offset-urlstart) + opentags.pop().etag;
220 // otherwise just close the tag
221 return opentags.pop().etag;
223 else if(m4 == "code" || m4 == "pre")
226 // other tags require no special processing, just output the end tag
227 return opentags.pop().etag;
234 // post must be HTML-encoded
236 function parseBBCode(post)
238 var result, endtags, tag;
240 // convert CRLF to <br> by default
243 // create a new array for open tags
244 if(opentags == null || opentags.length)
245 opentags = new Array(0);
247 // run the text through main regular expression matcher
248 result = post.replace(postfmt_re, textToHtmlCB);
250 // reset noparse, if it was unbalanced
254 // if there are any unbalanced tags, make sure to close them
255 if(opentags.length) {
256 endtags = new String();
258 // if there's an open [url] at the top, close it
259 if(opentags[opentags.length-1].bbtag == "url") {
261 endtags += "\">" + post.substr(urlstart, post.length-urlstart) + "</a>";
264 // close remaining open tags
265 while(opentags.length)
266 endtags += opentags.pop().etag;
269 return endtags ? result + endtags : result;