bbcode-js.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import { Dict } from "src/util";
  2. var VERSION = '0.4.0';
  3. export interface BBCodeConfig {
  4. showQuotePrefix?: boolean;
  5. classPrefix?: string;
  6. mentionPrefix?: string;
  7. }
  8. // default options
  9. const defaults: BBCodeConfig = {
  10. showQuotePrefix: true,
  11. classPrefix: 'bbcode_',
  12. mentionPrefix: '@'
  13. };
  14. export var version = VERSION;
  15. // copied from here:
  16. // http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the had to make an
  17. // update to allow / in the query string, since some sites will have a / there made another update to support colons in
  18. // the query string made another update to disallow an ending dot(.)
  19. var URL_PATTERN = new RegExp("(" // overall match
  20. + "(" // brackets covering match for protocol (optional) and domain
  21. + "([A-Za-z]{3,9}:(?:\\/\\/)?)" // allow something@ for email addresses
  22. + "(?:[\\-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]"
  23. // anything looking at all like a domain, non-unicode domains
  24. + "|" // or instead of above
  25. + "(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)" // starting with something@ or www.
  26. + "[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]" // anything looking at all like a domain
  27. + ")" // end protocol/domain
  28. + "(" // brackets covering match for path, query string and anchor
  29. + "(?:\\/[\\+~%\\/\\.\\w\\-_]*)?" // allow optional /path
  30. + "\\??(?:[\\-\\+=&;%@\\.\\w_\\/:]*)" // allow optional query string starting with ?
  31. + "#?(?:[\\.\\!\\/\\\\\\w]*)" // allow optional anchor #anchor
  32. + ")?" // make URL suffix optional
  33. + ")");
  34. function doReplace(content: string, matches: Replacement[], options: BBCodeConfig) {
  35. var i, obj, regex, hasMatch, tmp;
  36. // match/replace until we don't change the input anymore
  37. do {
  38. hasMatch = false;
  39. for (i = 0; i < matches.length; ++i) {
  40. obj = matches[i];
  41. regex = new RegExp(obj.e, 'gi');
  42. tmp = content.replace(regex, obj.func.bind(undefined, options));
  43. if (tmp !== content) {
  44. content = tmp;
  45. hasMatch = true;
  46. }
  47. }
  48. } while (hasMatch);
  49. return content;
  50. }
  51. function listItemReplace(options: BBCodeConfig, fullMatch: string, tag: string, value: string) {
  52. console.log(fullMatch);
  53. return '<li>' + doReplace(value.trim(), [{ e: '\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]', func: tagReplace }], options) + '</li>';
  54. }
  55. export var extractQuotedText = function (value: string, parts?: string[]) {
  56. var quotes = ["\"", "'"], i, quote, nextPart;
  57. for (i = 0; i < quotes.length; ++i) {
  58. quote = quotes[i];
  59. if (value && value[0] === quote) {
  60. value = value.slice(1);
  61. if (value[value.length - 1] !== quote) {
  62. while (parts && parts.length) {
  63. nextPart = parts.shift();
  64. value += " " + nextPart;
  65. if (nextPart[nextPart.length - 1] === quote) {
  66. break;
  67. }
  68. }
  69. }
  70. value = value.replace(new RegExp("[" + quote + "]+$"), '');
  71. break;
  72. }
  73. }
  74. return [value, parts];
  75. };
  76. export var parseParams = function (tagName: string, params: string) {
  77. let paramMap: Dict<string> = {};
  78. if (!params) {
  79. return paramMap;
  80. }
  81. // first, collapse spaces next to equals
  82. params = params.replace(/\s*[=]\s*/g, "=");
  83. let parts = params.split(/\s+/);
  84. while (parts.length) {
  85. let part = parts.shift();
  86. // check if the param itself is a valid url
  87. if (!URL_PATTERN.exec(part)) {
  88. let index = part.indexOf('=');
  89. if (index > 0) {
  90. let rv = extractQuotedText(part.slice(index + 1), parts);
  91. paramMap[part.slice(0, index).toLowerCase()] = rv[0] as string;
  92. parts = rv[1] as string[];
  93. }
  94. else {
  95. let rv = extractQuotedText(part, parts);
  96. paramMap[tagName] = rv[0] as string;
  97. parts = rv[1] as string[];
  98. }
  99. } else {
  100. let rv = extractQuotedText(part, parts);
  101. paramMap[tagName] = rv[0] as string;
  102. parts = rv[1] as string[];
  103. }
  104. }
  105. return paramMap;
  106. };
  107. function tagReplace(options: BBCodeConfig, fullMatch: string, tag: string, params: string, value: string) {
  108. let val: string;
  109. tag = tag.toLowerCase();
  110. let paramsObj = parseParams(tag, params || undefined);
  111. let inlineValue = paramsObj[tag];
  112. switch (tag) {
  113. case 'quote':
  114. val = '<div class="' + options.classPrefix + 'quote"';
  115. for (let i in paramsObj) {
  116. let tmp = paramsObj[i];
  117. if (!inlineValue && (i === 'author' || i === 'name')) {
  118. inlineValue = tmp;
  119. } else if (i !== tag) {
  120. val += ' data-' + i + '="' + tmp + '"';
  121. }
  122. }
  123. return val + '>' + (inlineValue ? inlineValue + ' wrote:' : (options.showQuotePrefix ? 'Quote:' : '')) + '<blockquote>' + value + '</blockquote></div>';
  124. case 'url':
  125. return '<a class="' + options.classPrefix + 'link" target="_blank" href="' + (inlineValue || value) + '">' + value + '</a>';
  126. case 'email':
  127. return '<a class="' + options.classPrefix + 'link" target="_blank" href="mailto:' + (inlineValue || value) + '">' + value + '</a>';
  128. case 'anchor':
  129. return '<a name="' + (inlineValue || paramsObj.a || value) + '">' + value + '</a>';
  130. case 'b':
  131. return '<strong>' + value + '</strong>';
  132. case 'i':
  133. return '<em>' + value + '</em>';
  134. case 'u':
  135. return '<span style="text-decoration:underline">' + value + '</span>';
  136. case 's':
  137. return '<span style="text-decoration:line-through">' + value + '</span>';
  138. case 'indent':
  139. return '<blockquote>' + value + '</blockquote>';
  140. case 'list':
  141. tag = 'ul';
  142. let className = options.classPrefix + 'list';
  143. if (inlineValue && /[1Aa]/.test(inlineValue)) {
  144. tag = 'ol';
  145. if (/1/.test(inlineValue)) {
  146. className += '_numeric';
  147. }
  148. else if (/A/.test(inlineValue)) {
  149. className += '_alpha';
  150. }
  151. else if (/a/.test(inlineValue)) {
  152. className += '_alpha_lower';
  153. }
  154. }
  155. val = '<' + tag + ' class="' + className + '">';
  156. // parse the value
  157. val += doReplace(value, [{ e: '\\[([*])\\]([^\r\n]+)', func: listItemReplace }], options);
  158. return val + '</' + tag + '>';
  159. case 'code':
  160. case 'php':
  161. case 'java':
  162. case 'javascript':
  163. case 'cpp':
  164. case 'ruby':
  165. case 'python':
  166. return '<pre class="' + options.classPrefix + (tag === 'code' ? '' : 'code_') + tag + '">' + value + '</pre>';
  167. case 'highlight':
  168. return '<span class="' + options.classPrefix + tag + '">' + value + '</span>';
  169. case 'html':
  170. return value;
  171. case 'mention':
  172. val = '<span class="' + options.classPrefix + 'mention"';
  173. if (inlineValue) {
  174. val += ' data-mention-id="' + inlineValue + '"';
  175. }
  176. return val + '>' + (options.mentionPrefix || '') + value + '</span>';
  177. case 'span':
  178. case 'h1':
  179. case 'h2':
  180. case 'h3':
  181. case 'h4':
  182. case 'h5':
  183. case 'h6':
  184. return '<' + tag + '>' + value + '</' + tag + '>';
  185. case 'youtube':
  186. return '<object class="' + options.classPrefix + 'video" width="425" height="350"><param name="movie" value="http://www.youtube.com/v/' + value + '"></param><embed src="http://www.youtube.com/v/' + value + '" type="application/x-shockwave-flash" width="425" height="350"></embed></object>';
  187. case 'gvideo':
  188. return '<embed class="' + options.classPrefix + 'video" style="width:400px; height:325px;" id="VideoPlayback" type="application/x-shockwave-flash" src="http://video.google.com/googleplayer.swf?docId=' + value + '&amp;hl=en">';
  189. case 'google':
  190. return '<a class="' + options.classPrefix + 'link" target="_blank" href="http://www.google.com/search?q=' + (inlineValue || value) + '">' + value + '</a>';
  191. case 'wikipedia':
  192. return '<a class="' + options.classPrefix + 'link" target="_blank" href="http://www.wikipedia.org/wiki/' + (inlineValue || value) + '">' + value + '</a>';
  193. case 'img':
  194. var dims = new RegExp('^(\\d+)x(\\d+)$').exec(inlineValue || '');
  195. if (!dims || (dims.length !== 3)) {
  196. dims = new RegExp('^width=(\\d+)\\s+height=(\\d+)$').exec(inlineValue || '');
  197. }
  198. if (dims && dims.length === 3) {
  199. params = undefined;
  200. }
  201. val = '<img class="' + options.classPrefix + 'image" src="' + value + '"';
  202. if (dims && dims.length === 3) {
  203. val += ' width="' + dims[1] + '" height="' + dims[2] + '"';
  204. } else {
  205. for (let i in paramsObj) {
  206. let tmp = paramsObj[i];
  207. if (i === 'img') {
  208. i = 'alt';
  209. }
  210. val += ' ' + i + '="' + tmp + '"';
  211. }
  212. }
  213. return val + '/>';
  214. }
  215. // return the original
  216. return fullMatch;
  217. }
  218. interface Replacement {
  219. e: string;
  220. func: (options: BBCodeConfig, fullMatch: string, tag: string, params: string, value: string) => string;
  221. }
  222. /**
  223. * Renders the content as html
  224. * @param content the given content to render
  225. * @param options optional object with control parameters
  226. * @returns rendered html
  227. */
  228. export var render = function (content: string, options?: BBCodeConfig) {
  229. var matches: Replacement[] = [], tmp;
  230. options = options || {};
  231. if (!options.classPrefix)
  232. options.classPrefix = defaults.classPrefix;
  233. if (!options.mentionPrefix)
  234. options.mentionPrefix = defaults.mentionPrefix;
  235. if (!options.showQuotePrefix)
  236. options.showQuotePrefix = defaults.showQuotePrefix;
  237. // for now, only one rule
  238. matches.push({ e: '\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]', func: tagReplace });
  239. return doReplace(content, matches, options);
  240. };