123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- import { Dict } from "src/util";
- const VERSION = "0.4.0";
- export interface BBCodeConfig {
- showQuotePrefix?: boolean;
- classPrefix?: string;
- mentionPrefix?: string;
- }
- // default options
- const defaults: BBCodeConfig = {
- showQuotePrefix: true,
- classPrefix: "bbcode_",
- mentionPrefix: "@"
- };
- export const 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(.)
- const 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) {
- let 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 "<li>" + doReplace(value.trim(), BBCODE_PATTERN, options) + "</li>";
- }
- export function extractQuotedText(value: string, parts?: string[]): (string | string[] | undefined)[] {
- const quotes = ["\"", "'"];
- let 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();
- if (!nextPart)
- continue;
- value += " " + nextPart;
- if (nextPart[nextPart.length - 1] === quote) {
- break;
- }
- }
- }
- value = value.replace(new RegExp("[" + quote + "]+$"), "");
- break;
- }
- }
- return [value, parts];
- }
- export function parseParams(tagName: string, params?: string): Dict<string> {
- const paramMap: Dict<string> = {};
- if (!params) {
- return paramMap;
- }
- // first, collapse spaces next to equals
- params = params.replace(/\s*[=]\s*/g, "=");
- let parts = params.split(/\s+/);
- while (parts.length) {
- const part = parts.shift() ?? "";
- // check if the param itself is a valid url
- if (!URL_PATTERN.exec(part)) {
- const index = part.indexOf("=");
- if (index > 0) {
- const rv = extractQuotedText(part.slice(index + 1), parts);
- paramMap[part.slice(0, index).toLowerCase()] = rv[0] as string;
- parts = rv[1] as string[];
- }
- else {
- const rv = extractQuotedText(part, parts);
- paramMap[tagName] = rv[0] as string;
- parts = rv[1] as string[];
- }
- } else {
- const 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();
- const 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 = "<div class=\"" + options.classPrefix + "quote\"";
- for (const i in paramsObj) {
- const tmp = paramsObj[i];
- if (!inlineValue && (i === "author" || i === "name")) {
- inlineValue = tmp;
- } else if (i !== tag) {
- val += " data-" + i + "=\"" + tmp + "\"";
- }
- }
- return val + ">" + (inlineValue ? inlineValue + " wrote:" : (options.showQuotePrefix ? "Quote:" : "")) + "<blockquote>" + value + "</blockquote></div>";
- case "url":
- return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"" + (inlineValue || value) + "\">" + value + "</a>";
- case "email":
- return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"mailto:" + (inlineValue || value) + "\">" + value + "</a>";
- case "anchor":
- return "<a name=\"" + (inlineValue || paramsObj.a || value) + "\">" + value + "</a>";
- case "b":
- return "<strong>" + value + "</strong>";
- case "i":
- return "<em>" + value + "</em>";
- case "u":
- return "<span style=\"text-decoration:underline\">" + value + "</span>";
- case "s":
- return "<span style=\"text-decoration:line-through\">" + value + "</span>";
- case "indent":
- return "<blockquote>" + value + "</blockquote>";
- 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 + "</" + tag + ">";
- }
- case "code":
- case "php":
- case "java":
- case "javascript":
- case "cpp":
- case "ruby":
- case "python":
- return "<pre class=\"" + options.classPrefix + (tag === "code" ? "" : "code_") + tag + "\">" + value + "</pre>";
- case "highlight":
- return "<span class=\"" + options.classPrefix + tag + "\">" + value + "</span>";
- case "html":
- return value;
- case "mention":
- val = "<span class=\"" + options.classPrefix + "mention\"";
- if (inlineValue) {
- val += " data-mention-id=\"" + inlineValue + "\"";
- }
- return val + ">" + (options.mentionPrefix || "") + value + "</span>";
- case "span":
- case "h1":
- case "h2":
- case "h3":
- case "h4":
- case "h5":
- case "h6":
- return "<" + tag + ">" + value + "</" + tag + ">";
- case "youtube":
- 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>";
- case "gvideo":
- 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 + "&hl=en\">";
- case "google":
- return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"http://www.google.com/search?q=" + (inlineValue || value) + "\">" + value + "</a>";
- case "wikipedia":
- return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"http://www.wikipedia.org/wiki/" + (inlineValue || value) + "\">" + value + "</a>";
- case "img": {
- let 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 = "<img class=\"" + options.classPrefix + "image\" src=\"" + value + "\"";
- if (dims && dims.length === 3) {
- val += " width=\"" + dims[1] + "\" height=\"" + dims[2] + "\"";
- } else {
- for (let i in paramsObj) {
- const tmp = paramsObj[i];
- if (i === "img") {
- i = "alt";
- }
- val += " " + i + "=\"" + tmp + "\"";
- }
- }
- return 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 function render(content: string, options?: BBCodeConfig): string {
- 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);
- }
|