OSDN Git Service

テストケースを追加した
[webchat/WebChat.git] / public / scripts / bbcode.js
1 // -----------------------------------------------------------------------
2 // Copyright (c) 2008, Stone Steps Inc. 
3 // All rights reserved
4 // http://www.stonesteps.ca/legal/bsd-license/
5 //
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.
9 //
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. 
12 //
13 // Following BBCode expressions are recognized:
14 //
15 // [b]bold[/b]
16 // [i]italic[/i]
17 // [u]underlined[/u]
18 // [s]strike-through[/s]
19 // [samp]sample[/samp]
20 //
21 // [color=red]red[/color]
22 // [color=#FF0000]red[/color]
23 // [size=1.2]1.2em[/size]
24 //
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]
27 //
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]
32 //
33 // [pre]formatted 
34 //     text[/pre]
35 // [code]if(a == b) 
36 //   print("done");[/code]
37 //
38 // text containing [noparse] [brackets][/noparse]
39 //
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)
45
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)$/;
48
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;
51
52 // numbers
53 var number_re = /^[\\.0-9]{1,8}$/i;
54
55 // reserved, unreserved, escaped and alpha-numeric [RFC2396]
56 var uri_re = /^[-;\/\?:@&=\+\$,_\.!~\*'\(\)%0-9a-z]{1,512}$/i;
57
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;
60
61 // stack frame object
62 function taginfo_t(bbtag, etag)
63 {
64    this.bbtag = bbtag;
65    this.etag = etag;
66 }
67
68 // check if it's a valid BBCode tag
69 function isValidTag(str)
70 {
71    if(!str || !str.length)
72       return false;
73
74    return tagname_re.test(str);
75 }
76
77 //
78 // m1 - CR or LF
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
82 //
83 function textToHtmlCB(mstr, m1, m2, m3, m4, m5, offset, string)
84 {
85    //
86    // CR LF sequences
87    //
88    if(m1 && m1.length) {
89       if(!crlf2br)
90          return mstr;
91
92       switch (m1) {
93          case '\r':
94             return "";
95          case '\n':
96             return "<br>";
97       }
98    }
99
100    if(isValidTag(m5)){
101       // if in the noparse state, just echo the tag
102       if(noparse)
103          return "[" + m5 + "/]";
104
105       switch (m5) {
106          case "br":
107             crlf2br = false;
108             return "<br/>";
109       }
110    }
111    //
112    // handle start tags
113    //
114    if(isValidTag(m2)) {
115       // if in the noparse state, just echo the tag
116       if(noparse)
117          return "[" + m2 + "]";
118
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 + "]";
122
123       switch (m2) {
124          case "code":
125             opentags.push(new taginfo_t(m2, "</code></pre>"));
126             crlf2br = false;
127             return "<pre><code>";
128
129          case "pre":
130             opentags.push(new taginfo_t(m2, "</pre>"));
131             crlf2br = false;
132             return "<pre>";
133
134          case "color":
135          case "colour":
136             if(!m3 || !color_re.test(m3))
137                m3 = "inherit";
138             opentags.push(new taginfo_t(m2, "</span>"));
139             return "<span style=\"color: " + m3 + "\">";
140
141          case "size":
142             if(!m3 || !number_re.test(m3))
143                m3 = "1";
144             opentags.push(new taginfo_t(m2, "</span>"));
145             return "<span style=\"font-size: " + Math.min(Math.max(m3, 0.7), 3) + "em\">";
146
147          case "s":
148             opentags.push(new taginfo_t(m2, "</span>"));
149             return "<span style=\"text-decoration: line-through\">";
150
151          case "noparse":
152             noparse = true;
153             return "";
154
155          case "url":
156             opentags.push(new taginfo_t(m2, "</a>"));
157             
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
161                urlstart = -1;
162                return "<a href=\"" + m3 + "\">";
163             }
164
165             // otherwise, remember the URL offset 
166             urlstart = mstr.length + offset;
167
168             // and treat the text following [url] as a URL
169             return "<a href=\"";
170
171          case "q":
172          case "blockquote":
173             opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
174             return m3 && m3.length && uri_re.test(m3) ? "<" + m2 + " cite=\"" + m3 + "\">" : "<" + m2 + ">";
175
176          case "b":
177             opentags.push(new taginfo_t(m2, "</span>"));
178             return "<span style=\"font-weight: bold\">";
179
180          case "i":
181             opentags.push(new taginfo_t(m2, "</span>"));
182             return "<span style=\"font-style:italic\">";
183
184          case "u":
185             opentags.push(new taginfo_t(m2, "</span>"));
186             return "<span style=\"text-decoration: underline\">";
187
188          default:
189             // [samp]don't need special processing
190             opentags.push(new taginfo_t(m2, "</" + m2 + ">"));
191             return "<" + m2 + ">";
192             
193       }
194    }
195
196    //
197    // process end tags
198    //
199    if(isValidTag(m4)) {
200       if(noparse) {
201          // if it's the closing noparse tag, flip the noparse state
202          if(m4 == "noparse")  {
203             noparse = false;
204             return "";
205          }
206          
207          // otherwise just output the original text
208          return "[/" + m4 + "]";
209       }
210       
211       // highlight mismatched end tags
212       if(!opentags.length || opentags[opentags.length-1].bbtag != m4)
213          return "<span style=\"color: red\">[/" + m4 + "]</span>";
214
215       if(m4 == "url") {
216          // if there was no option, use the content of the [url] tag
217          if(urlstart > 0)
218             return "\">" + string.substr(urlstart, offset-urlstart) + opentags.pop().etag;
219          
220          // otherwise just close the tag
221          return opentags.pop().etag;
222       }
223       else if(m4 == "code" || m4 == "pre")
224          crlf2br = true;
225
226       // other tags require no special processing, just output the end tag
227       return opentags.pop().etag;
228    }
229
230    return mstr;
231 }
232
233 //
234 // post must be HTML-encoded
235 //
236 function parseBBCode(post)
237 {
238    var result, endtags, tag;
239
240    // convert CRLF to <br> by default
241    crlf2br = true;
242
243    // create a new array for open tags
244    if(opentags == null || opentags.length)
245       opentags = new Array(0);
246
247    // run the text through main regular expression matcher
248    result = post.replace(postfmt_re, textToHtmlCB);
249
250    // reset noparse, if it was unbalanced
251    if(noparse)
252       noparse = false;
253    
254    // if there are any unbalanced tags, make sure to close them
255    if(opentags.length) {
256       endtags = new String();
257       
258       // if there's an open [url] at the top, close it
259       if(opentags[opentags.length-1].bbtag == "url") {
260          opentags.pop();
261          endtags += "\">" + post.substr(urlstart, post.length-urlstart) + "</a>";
262       }
263       
264       // close remaining open tags
265       while(opentags.length)
266          endtags += opentags.pop().etag;
267    }
268
269    return endtags ? result + endtags : result;
270 }