import { Dict } from "src/util"; var VERSION = '0.4.0'; export interface BBCodeConfig { showQuotePrefix?: boolean; classPrefix?: string; mentionPrefix?: string; } // default options const defaults: BBCodeConfig = { showQuotePrefix: true, classPrefix: 'bbcode_', mentionPrefix: '@' }; export var version = VERSION; // copied from here: // http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the had to make an // update to allow / in the query string, since some sites will have a / there made another update to support colons in // the query string made another update to disallow an ending dot(.) var URL_PATTERN = new RegExp("(" // overall match + "(" // brackets covering match for protocol (optional) and domain + "([A-Za-z]{3,9}:(?:\\/\\/)?)" // allow something@ for email addresses + "(?:[\\-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]" // anything looking at all like a domain, non-unicode domains + "|" // or instead of above + "(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)" // starting with something@ or www. + "[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]" // anything looking at all like a domain + ")" // end protocol/domain + "(" // brackets covering match for path, query string and anchor + "(?:\\/[\\+~%\\/\\.\\w\\-_]*)?" // allow optional /path + "\\??(?:[\\-\\+=&;%@\\.\\w_\\/:]*)" // allow optional query string starting with ? + "#?(?:[\\.\\!\\/\\\\\\w]*)" // allow optional anchor #anchor + ")?" // make URL suffix optional + ")"); function doReplace(content: string, matches: Replacement[], options: BBCodeConfig) { var i, obj, regex, hasMatch, tmp; // match/replace until we don't change the input anymore do { hasMatch = false; for (i = 0; i < matches.length; ++i) { obj = matches[i]; regex = new RegExp(obj.e, 'gi'); tmp = content.replace(regex, obj.func.bind(undefined, options)); if (tmp !== content) { content = tmp; hasMatch = true; } } } while (hasMatch); return content; } function listItemReplace(options: BBCodeConfig, fullMatch: string, tag: string, value: string) { return '
  • ' + doReplace(value.trim(), BBCODE_PATTERN, options) + '
  • '; } export var extractQuotedText = function (value: string, parts?: string[]) { var quotes = ["\"", "'"], i, quote, nextPart; for (i = 0; i < quotes.length; ++i) { quote = quotes[i]; if (value && value[0] === quote) { value = value.slice(1); if (value[value.length - 1] !== quote) { while (parts && parts.length) { nextPart = parts.shift(); value += " " + nextPart; if (nextPart![nextPart!.length - 1] === quote) { break; } } } value = value.replace(new RegExp("[" + quote + "]+$"), ''); break; } } return [value, parts]; }; export var parseParams = function (tagName: string, params?: string) { let paramMap: Dict = {}; if (!params) { return paramMap; } // first, collapse spaces next to equals params = params.replace(/\s*[=]\s*/g, "="); let parts = params.split(/\s+/); while (parts.length) { let part = parts.shift() ?? ""; // check if the param itself is a valid url if (!URL_PATTERN.exec(part)) { let index = part.indexOf('='); if (index > 0) { let rv = extractQuotedText(part.slice(index + 1), parts); paramMap[part.slice(0, index).toLowerCase()] = rv[0] as string; parts = rv[1] as string[]; } else { let rv = extractQuotedText(part, parts); paramMap[tagName] = rv[0] as string; parts = rv[1] as string[]; } } else { let rv = extractQuotedText(part, parts); paramMap[tagName] = rv[0] as string; parts = rv[1] as string[]; } } return paramMap; }; const BBCODE_PATTERN = [{ e: '\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]', func: tagReplace }]; function tagReplace(options: BBCodeConfig, fullMatch: string, tag: string, params: string | undefined, value: string) { let val: string; tag = tag.toLowerCase(); let paramsObj = parseParams(tag, params || undefined); let inlineValue = paramsObj[tag]; switch (tag) { case 'attach': return ''; case 'spoiler': return ''; case 'center': return doReplace(value, BBCODE_PATTERN, options); case 'size': return doReplace(value, BBCODE_PATTERN, options); case 'quote': val = '
    ' + (inlineValue ? inlineValue + ' wrote:' : (options.showQuotePrefix ? 'Quote:' : '')) + '
    ' + value + '
    '; case 'url': return '' + value + ''; case 'email': return '' + value + ''; case 'anchor': return '' + value + ''; case 'b': return '' + value + ''; case 'i': return '' + value + ''; case 'u': return '' + value + ''; case 's': return '' + value + ''; case 'indent': return '
    ' + value + '
    '; case 'list': tag = 'ul'; let className = options.classPrefix + 'list'; if (inlineValue && /[1Aa]/.test(inlineValue)) { tag = 'ol'; if (/1/.test(inlineValue)) { className += '_numeric'; } else if (/A/.test(inlineValue)) { className += '_alpha'; } else if (/a/.test(inlineValue)) { className += '_alpha_lower'; } } val = '<' + tag + ' class="' + className + '">'; // parse the value val += doReplace(value, [{ e: '\\[([*])\\]([^\r\n]+)', func: listItemReplace }], options); return val + ''; case 'code': case 'php': case 'java': case 'javascript': case 'cpp': case 'ruby': case 'python': return '
    ' + value + '
    '; case 'highlight': return '' + value + ''; case 'html': return value; case 'mention': val = '' + (options.mentionPrefix || '') + value + ''; case 'span': case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': return '<' + tag + '>' + value + ''; case 'youtube': return ''; case 'gvideo': return ''; case 'google': return '' + value + ''; case 'wikipedia': return '' + value + ''; case 'img': var dims = new RegExp('^(\\d+)x(\\d+)$').exec(inlineValue || ''); if (!dims || (dims.length !== 3)) { dims = new RegExp('^width=(\\d+)\\s+height=(\\d+)$').exec(inlineValue || ''); } if (dims && dims.length === 3) { params = undefined; } val = ''; } // return the original return fullMatch; } interface Replacement { e: string; func: (options: BBCodeConfig, fullMatch: string, tag: string, params: string, value: string) => string; } /** * Renders the content as html * @param content the given content to render * @param options optional object with control parameters * @returns rendered html */ export var render = function (content: string, options?: BBCodeConfig) { options = options || {}; if (!options.classPrefix) options.classPrefix = defaults.classPrefix; if (!options.mentionPrefix) options.mentionPrefix = defaults.mentionPrefix; if (!options.showQuotePrefix) options.showQuotePrefix = defaults.showQuotePrefix; // for now, only one rule return doReplace(content, BBCODE_PATTERN, options); };