bbcode-js.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. return '<li>' + doReplace(value.trim(), BBCODE_PATTERN, options) + '</li>';
  53. }
  54. export var extractQuotedText = function (value: string, parts?: string[]) {
  55. var quotes = ["\"", "'"], i, quote, nextPart;
  56. for (i = 0; i < quotes.length; ++i) {
  57. quote = quotes[i];
  58. if (value && value[0] === quote) {
  59. value = value.slice(1);
  60. if (value[value.length - 1] !== quote) {
  61. while (parts && parts.length) {
  62. nextPart = parts.shift();
  63. value += " " + nextPart;
  64. if (nextPart[nextPart.length - 1] === quote) {
  65. break;
  66. }
  67. }
  68. }
  69. value = value.replace(new RegExp("[" + quote + "]+$"), '');
  70. break;
  71. }
  72. }
  73. return [value, parts];
  74. };
  75. export var parseParams = function (tagName: string, params: string) {
  76. let paramMap: Dict<string> = {};
  77. if (!params) {
  78. return paramMap;
  79. }
  80. // first, collapse spaces next to equals
  81. params = params.replace(/\s*[=]\s*/g, "=");
  82. let parts = params.split(/\s+/);
  83. while (parts.length) {
  84. let part = parts.shift();
  85. // check if the param itself is a valid url
  86. if (!URL_PATTERN.exec(part)) {
  87. let index = part.indexOf('=');
  88. if (index > 0) {
  89. let rv = extractQuotedText(part.slice(index + 1), parts);
  90. paramMap[part.slice(0, index).toLowerCase()] = rv[0] as string;
  91. parts = rv[1] as string[];
  92. }
  93. else {
  94. let rv = extractQuotedText(part, parts);
  95. paramMap[tagName] = rv[0] as string;
  96. parts = rv[1] as string[];
  97. }
  98. } else {
  99. let rv = extractQuotedText(part, parts);
  100. paramMap[tagName] = rv[0] as string;
  101. parts = rv[1] as string[];
  102. }
  103. }
  104. return paramMap;
  105. };
  106. const BBCODE_PATTERN = [{ e: '\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]', func: tagReplace }];
  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 'attach':
  114. return '';
  115. case 'spoiler':
  116. return '';
  117. case 'center':
  118. return doReplace(value, BBCODE_PATTERN, options);
  119. case 'size':
  120. return doReplace(value, BBCODE_PATTERN, options);
  121. case 'quote':
  122. val = '<div class="' + options.classPrefix + 'quote"';
  123. for (let i in paramsObj) {
  124. let tmp = paramsObj[i];
  125. if (!inlineValue && (i === 'author' || i === 'name')) {
  126. inlineValue = tmp;
  127. } else if (i !== tag) {
  128. val += ' data-' + i + '="' + tmp + '"';
  129. }
  130. }
  131. return val + '>' + (inlineValue ? inlineValue + ' wrote:' : (options.showQuotePrefix ? 'Quote:' : '')) + '<blockquote>' + value + '</blockquote></div>';
  132. case 'url':
  133. return '<a class="' + options.classPrefix + 'link" target="_blank" href="' + (inlineValue || value) + '">' + value + '</a>';
  134. case 'email':
  135. return '<a class="' + options.classPrefix + 'link" target="_blank" href="mailto:' + (inlineValue || value) + '">' + value + '</a>';
  136. case 'anchor':
  137. return '<a name="' + (inlineValue || paramsObj.a || value) + '">' + value + '</a>';
  138. case 'b':
  139. return '<strong>' + value + '</strong>';
  140. case 'i':
  141. return '<em>' + value + '</em>';
  142. case 'u':
  143. return '<span style="text-decoration:underline">' + value + '</span>';
  144. case 's':
  145. return '<span style="text-decoration:line-through">' + value + '</span>';
  146. case 'indent':
  147. return '<blockquote>' + value + '</blockquote>';
  148. case 'list':
  149. tag = 'ul';
  150. let className = options.classPrefix + 'list';
  151. if (inlineValue && /[1Aa]/.test(inlineValue)) {
  152. tag = 'ol';
  153. if (/1/.test(inlineValue)) {
  154. className += '_numeric';
  155. }
  156. else if (/A/.test(inlineValue)) {
  157. className += '_alpha';
  158. }
  159. else if (/a/.test(inlineValue)) {
  160. className += '_alpha_lower';
  161. }
  162. }
  163. val = '<' + tag + ' class="' + className + '">';
  164. // parse the value
  165. val += doReplace(value, [{ e: '\\[([*])\\]([^\r\n]+)', func: listItemReplace }], options);
  166. return val + '</' + tag + '>';
  167. case 'code':
  168. case 'php':
  169. case 'java':
  170. case 'javascript':
  171. case 'cpp':
  172. case 'ruby':
  173. case 'python':
  174. return '<pre class="' + options.classPrefix + (tag === 'code' ? '' : 'code_') + tag + '">' + value + '</pre>';
  175. case 'highlight':
  176. return '<span class="' + options.classPrefix + tag + '">' + value + '</span>';
  177. case 'html':
  178. return value;
  179. case 'mention':
  180. val = '<span class="' + options.classPrefix + 'mention"';
  181. if (inlineValue) {
  182. val += ' data-mention-id="' + inlineValue + '"';
  183. }
  184. return val + '>' + (options.mentionPrefix || '') + value + '</span>';
  185. case 'span':
  186. case 'h1':
  187. case 'h2':
  188. case 'h3':
  189. case 'h4':
  190. case 'h5':
  191. case 'h6':
  192. return '<' + tag + '>' + value + '</' + tag + '>';
  193. case 'youtube':
  194. 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>';
  195. case 'gvideo':
  196. 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">';
  197. case 'google':
  198. return '<a class="' + options.classPrefix + 'link" target="_blank" href="http://www.google.com/search?q=' + (inlineValue || value) + '">' + value + '</a>';
  199. case 'wikipedia':
  200. return '<a class="' + options.classPrefix + 'link" target="_blank" href="http://www.wikipedia.org/wiki/' + (inlineValue || value) + '">' + value + '</a>';
  201. case 'img':
  202. var dims = new RegExp('^(\\d+)x(\\d+)$').exec(inlineValue || '');
  203. if (!dims || (dims.length !== 3)) {
  204. dims = new RegExp('^width=(\\d+)\\s+height=(\\d+)$').exec(inlineValue || '');
  205. }
  206. if (dims && dims.length === 3) {
  207. params = undefined;
  208. }
  209. val = '<img class="' + options.classPrefix + 'image" src="' + value + '"';
  210. if (dims && dims.length === 3) {
  211. val += ' width="' + dims[1] + '" height="' + dims[2] + '"';
  212. } else {
  213. for (let i in paramsObj) {
  214. let tmp = paramsObj[i];
  215. if (i === 'img') {
  216. i = 'alt';
  217. }
  218. val += ' ' + i + '="' + tmp + '"';
  219. }
  220. }
  221. return val + '/>';
  222. }
  223. // return the original
  224. return fullMatch;
  225. }
  226. interface Replacement {
  227. e: string;
  228. func: (options: BBCodeConfig, fullMatch: string, tag: string, params: string, value: string) => string;
  229. }
  230. /**
  231. * Renders the content as html
  232. * @param content the given content to render
  233. * @param options optional object with control parameters
  234. * @returns rendered html
  235. */
  236. export var render = function (content: string, options?: BBCodeConfig) {
  237. options = options || {};
  238. if (!options.classPrefix)
  239. options.classPrefix = defaults.classPrefix;
  240. if (!options.mentionPrefix)
  241. options.mentionPrefix = defaults.mentionPrefix;
  242. if (!options.showQuotePrefix)
  243. options.showQuotePrefix = defaults.showQuotePrefix;
  244. // for now, only one rule
  245. return doReplace(content, BBCODE_PATTERN, options);
  246. };