544 lines
21 KiB
JavaScript
544 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
(function() {
|
|
|
|
var defaultTags = [
|
|
// inline formatting
|
|
'b', 'code', 'em', 'i', 's', 'strong', 'sub', 'sup', 'u',
|
|
// block formatting
|
|
'blockquote', 'h1', 'h2', 'h3', 'p', 'pre',
|
|
// lists
|
|
'li', 'ol', 'ul',
|
|
// tables
|
|
'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr',
|
|
// other
|
|
'a', 'br', 'img',
|
|
// special
|
|
'rtl', '[]'
|
|
],
|
|
htmlEntities = {
|
|
'"': '"', '&': '&', "'": ''', '<': '<', '>': '>'
|
|
},
|
|
regexp = {
|
|
entity: /&[^\s]+?;/g,
|
|
html: /[<&]/,
|
|
tag: new RegExp('<\\/?(' + [
|
|
'a', 'b', 'br', 'code', 'i', 's', 'span', 'u'
|
|
].join('|') + ')\\/?>', 'gi')
|
|
},
|
|
replace = {
|
|
a: [
|
|
[
|
|
/<a [^<>]*?href="((\/|https?:\/\/|mailto:).+?)".*?>/gi,
|
|
'<a href="{1}">',
|
|
],
|
|
[
|
|
/<\/a>/gi,
|
|
'</a>'
|
|
]
|
|
],
|
|
img: [
|
|
[
|
|
/<img [^<>]*?src="((\/|https?:\/\/).+?)".*?>/gi,
|
|
'<img src="$1">'
|
|
]
|
|
],
|
|
rtl: [
|
|
[
|
|
/<rtl>/gi,
|
|
'<div style="direction: rtl">'
|
|
],
|
|
[
|
|
/<\/rtl>/gi,
|
|
'</div>'
|
|
]
|
|
],
|
|
'*': function(tag) {
|
|
return [
|
|
[
|
|
new RegExp('</?' + tag + ' ?/?>', 'gi'),
|
|
'{0}'
|
|
]
|
|
];
|
|
}
|
|
},
|
|
salt = Ox.range(2).map(function(){
|
|
return Ox.range(16).map(function() {
|
|
return String.fromCharCode(65 + Ox.random(26));
|
|
}).join('');
|
|
});
|
|
|
|
function addLinks(string, obfuscate) {
|
|
return string
|
|
.replace(
|
|
/\b((https?:\/\/|www\.).+?)([.,:;!?)\]]*?(\s|$))/gi,
|
|
function(match, url, prefix, end) {
|
|
prefix = prefix.toLowerCase() == 'www.' ? 'http://' : '';
|
|
return Ox.formatString(
|
|
'<a href="{prefix}{url}">{url}</a>{end}',
|
|
{end: end, prefix: prefix, url: url}
|
|
);
|
|
}
|
|
)
|
|
.replace(
|
|
/\b([0-9A-Z.+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6})\b/gi,
|
|
obfuscate ? function(match, mail) {
|
|
return Ox.encodeEmailAddress(mail);
|
|
} : '<a href="mailto:$1">$1</a>'
|
|
);
|
|
}
|
|
|
|
function decodeHTMLEntities(string) {
|
|
return string
|
|
.replace(
|
|
new RegExp('(' + Ox.values(htmlEntities).join('|') + ')', 'g'),
|
|
function(match) {
|
|
return Ox.keyOf(htmlEntities, match);
|
|
}
|
|
)
|
|
.replace(
|
|
/&#([0-9A-FX]+);/gi,
|
|
function(match, code) {
|
|
return Ox.char(
|
|
/^X/i.test(code)
|
|
? parseInt(code.slice(1), 16)
|
|
: parseInt(code, 10)
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
// Splits a string into text (even indices) and tags (odd indices), ignoring
|
|
// tags with starting positions that are included in the ignore array
|
|
function splitHTMLTags(string, ignore) {
|
|
var isTag = false, ret = [''];
|
|
ignore = ignore || [];
|
|
Ox.forEach(string, function(char, i) {
|
|
if (!isTag && char == '<' && ignore.indexOf(i) == -1) {
|
|
isTag = true;
|
|
ret.push('');
|
|
}
|
|
ret[ret.length - 1] += char;
|
|
if (isTag && char == '>') {
|
|
isTag = false;
|
|
ret.push('');
|
|
}
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
/*@
|
|
Ox.addLinks <f> Takes a string and adds links for e-mail addresses and URLs
|
|
(string[, isHTML]) -> <s> Formatted string
|
|
string <s> String
|
|
isHTML <b|false> If true, ignore matches in tags or enclosed by links
|
|
> Ox.addLinks('foo bar <foo@bar.com>')
|
|
'foo bar <<a href="mailto:foo@bar.com">foo@bar.com</a>>'
|
|
> Ox.addLinks('www.foo.com/bar#baz, etc.')
|
|
'<a href="http://www.foo.com/bar#baz">www.foo.com/bar#baz</a>, etc.'
|
|
> Ox.addLinks('<a href="http://www.foo.com">www.foo.com</a>', true)
|
|
'<a href="http://www.foo.com">www.foo.com</a>'
|
|
@*/
|
|
Ox.addLinks = function(string, isHTML) {
|
|
var isLink = false;
|
|
return isHTML
|
|
? splitHTMLTags(string).map(function(string, i) {
|
|
var isTag = i % 2;
|
|
if (isTag) {
|
|
if (/^<a/.test(string)) {
|
|
isLink = true;
|
|
} else if (/^<\/a/.test(string)) {
|
|
isLink = false;
|
|
}
|
|
}
|
|
return isTag || isLink ? string : addLinks(string);
|
|
}).join('')
|
|
: Ox.normalizeHTML(addLinks(string));
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeEmailAddress <f> Returns obfuscated mailto: link
|
|
> Ox.encodeEmailAddress('mailto:foo@bar.com').indexOf(':') > -1
|
|
true
|
|
@*/
|
|
Ox.encodeEmailAddress = function(string) {
|
|
var parts = ['mailto:' + string, string].map(function(part) {
|
|
return Ox.map(part, function(char) {
|
|
var code = char.charCodeAt(0);
|
|
return char == ':' ? ':'
|
|
: '&#'
|
|
+ (Math.random() < 0.5 ? code : 'x' + code.toString(16))
|
|
+ ';'
|
|
});
|
|
});
|
|
return '<a href="' + parts[0] + '">' + parts[1] + '</a>';
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeHTMLEntities <f> Encodes HTML entities
|
|
(string[, encodeAll]) -> <s> String
|
|
string <s> String
|
|
encodeAll <b|false> If true, encode characters > 127 as numeric entities
|
|
> Ox.encodeHTMLEntities('<\'&"> äbçdê')
|
|
'<'&"> äbçdê'
|
|
> Ox.encodeHTMLEntities('<\'&"> äbçdê', true)
|
|
'<'&"> äbçdê'
|
|
@*/
|
|
Ox.encodeHTMLEntities = function(string, encodeAll) {
|
|
return Ox.map(String(string), function(char) {
|
|
var code = char.charCodeAt(0);
|
|
if (code < 128) {
|
|
char = char in htmlEntities ? htmlEntities[char] : char;
|
|
} else if (encodeAll) {
|
|
char = '&#x'
|
|
+ Ox.pad(code.toString(16).toUpperCase(), 'left', 4, '0')
|
|
+ ';';
|
|
}
|
|
return char;
|
|
});
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeHTMLEntities <f> Decodes HTML entities
|
|
(string[, decodeAll]) -> <s> String
|
|
string <s> String
|
|
decodeAll <b|false> If true, decode named entities for characters > 127
|
|
Note that `decodeAll` relies on `Ox.normalizeHTML`, which uses the
|
|
DOM and may transform the string
|
|
> Ox.decodeHTMLEntities('<'&">')
|
|
'<\'&">'
|
|
> Ox.decodeHTMLEntities('<'&">')
|
|
'<\'&">'
|
|
> Ox.decodeHTMLEntities('äbçdê')
|
|
'äbçdê'
|
|
> Ox.decodeHTMLEntities('äbçdê')
|
|
'äbçdê'
|
|
> Ox.decodeHTMLEntities('äbçdê', true)
|
|
'äbçdê'
|
|
> Ox.decodeHTMLEntities('<b>β')
|
|
'<b>β'
|
|
> Ox.decodeHTMLEntities('<b>β', true)
|
|
'<b>β</b>'
|
|
> Ox.decodeHTMLEntities('<b>')
|
|
'<b>'
|
|
@*/
|
|
Ox.decodeHTMLEntities = function(string, decodeAll) {
|
|
return decodeAll
|
|
? Ox.decodeHTMLEntities(Ox.normalizeHTML(string))
|
|
: decodeHTMLEntities(string);
|
|
};
|
|
|
|
/*@
|
|
Ox.highlight <f> Highlight matches in string
|
|
(string, query, classname[, isHTML]) -> Output string
|
|
string <s> Input string
|
|
query <r|s> Case-insentitive query string, or regular expression
|
|
classname <s> Class name for matches
|
|
isHTML <b|false> If true, the input string is treated as HTML
|
|
> Ox.highlight('<foo><bar>', 'foo', 'c')
|
|
'<<span class="c">foo</span>><bar>'
|
|
> Ox.highlight('&', '&', 'c')
|
|
'<span class="c">&amp;</span>'
|
|
> Ox.highlight('&', '&', 'c')
|
|
'&'
|
|
> Ox.highlight('<foo> <foo>', '<foo>', 'c', true)
|
|
'<span class="c"><foo></span> <span class="c"><foo></span>'
|
|
> Ox.highlight('<span class="name">name</span>', 'name', 'c', true)
|
|
'<span class="name"><span class="c">name</span></span>'
|
|
> Ox.highlight('amp & amp', 'amp', 'c', true)
|
|
'<span class="c">amp</span> & <span class="c">amp</span>'
|
|
> Ox.highlight('amp & amp', 'amp & amp', 'c', true)
|
|
'<span class="c">amp & amp</span>'
|
|
> Ox.highlight('<b><b></b>', '<b>', 'c', true)
|
|
'<span class="c"><b><b></b></span>'
|
|
> Ox.highlight('<b><b></b>', '<b>', 'c', true)
|
|
'<b><b></b>'
|
|
> Ox.highlight('foo<b>bar</b>baz', 'foobar', 'c', true)
|
|
'<span class="c">foo<b>bar</b></span>baz'
|
|
> Ox.highlight('foo<p>bar</p>baz', 'foobar', 'c', true)
|
|
'foo<p>bar</p>baz'
|
|
> Ox.highlight('foo <br/>bar baz', 'foo bar', 'c', true)
|
|
'<span class="c">foo <br>bar</span> baz'
|
|
@*/
|
|
Ox.highlight = function(string, query, classname, isHTML) {
|
|
if (!query) {
|
|
return string;
|
|
}
|
|
var cursor = 0,
|
|
entities = [],
|
|
matches = [],
|
|
offset = 0,
|
|
re = Ox.isRegExp(query) ? query
|
|
: new RegExp(Ox.escapeRegExp(query), 'gi'),
|
|
span = ['<span class="' + classname + '">', '</span>'],
|
|
tags = [];
|
|
function insert(array) {
|
|
// for each replacement
|
|
array.forEach(function(v) {
|
|
// replace the modified value with the original value
|
|
string = Ox.splice(string, v.position, v.length, v.value);
|
|
// for each match
|
|
matches.forEach(function(match) {
|
|
if (v.position < match.position) {
|
|
// replacement is before match, update match position
|
|
match.position += v.value.length - v.length;
|
|
} else if (
|
|
v.position < match.position + match.value.length
|
|
) {
|
|
// replacement is inside match, update match value
|
|
match.value = Ox.splice(
|
|
match.value, v.position - match.position, v.length,
|
|
v.value
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
if (isHTML && regexp.html.test(string)) {
|
|
string = string // Ox.normalizeHTML(string)
|
|
// remove inline tags
|
|
.replace(regexp.tag, function(value, tag, position) {
|
|
tags.push({
|
|
length: 0, position: position, value: value
|
|
});
|
|
return '';
|
|
})
|
|
// decode html entities
|
|
.replace(regexp.entity, function(value, position) {
|
|
var ret = Ox.decodeHTMLEntities(value, true);
|
|
entities.push({
|
|
length: ret.length, position: position, value: value
|
|
});
|
|
return ret;
|
|
});
|
|
// if decoding entities has created new tags, ignore them
|
|
splitHTMLTags(string, entities.map(function(entity) {
|
|
var ret = entity.position + offset;
|
|
offset += entity.length - entity.value.length
|
|
return ret;
|
|
})).forEach(function(v, i) {
|
|
if (i % 2 == 0) {
|
|
// outside tags, find matches and save position and value
|
|
v.replace(re, function(value, position) {
|
|
matches.push(
|
|
{position: cursor + position, value: value}
|
|
);
|
|
});
|
|
}
|
|
cursor += v.length;
|
|
});
|
|
insert(entities);
|
|
insert(tags);
|
|
// for each match (in reverse order, so that positions are correct)
|
|
matches.reverse().forEach(function(match) {
|
|
// wrap it in a span
|
|
string = Ox.splice(
|
|
string, match.position, match.value.length,
|
|
span.join(match.value)
|
|
);
|
|
});
|
|
// we may have enclosed single opening or closing tags in a span
|
|
if (matches.length && tags.length) {
|
|
string = Ox.normalizeHTML(string);
|
|
}
|
|
} else {
|
|
string = Ox.encodeHTMLEntities(
|
|
string.replace(re, function(value) {
|
|
matches.push(span.join(Ox.encodeHTMLEntities(value)));
|
|
return salt.join(matches.length - 1);
|
|
})
|
|
);
|
|
matches.forEach(function(match, i) {
|
|
string = string.replace(new RegExp(salt.join(i)), match);
|
|
});
|
|
}
|
|
return string;
|
|
};
|
|
|
|
/*@
|
|
Ox.normalizeHTML <f> Normalize HTML (using the DOM)
|
|
> Ox.normalizeHTML('<b>foo')
|
|
'<b>foo</b>'
|
|
> Ox.normalizeHTML('<b>foo</b></b>')
|
|
'<b>foo</b>'
|
|
> Ox.normalizeHTML('<'&"> äbçdê')
|
|
'<\'&"> äbçdê'
|
|
@*/
|
|
Ox.normalizeHTML = function(html) {
|
|
return regexp.html.test(html) ? Ox.$('<div>').html(html).html() : html;
|
|
};
|
|
|
|
/*@
|
|
Ox.parseMarkdown <f> Parses (a tiny subset of) Markdown.
|
|
Supports `*emphasis*`, `_emphasis_`, `**strong**`, `__strong__`,
|
|
`` `code` ``, ``` ``code with backtick (`)`` ```,
|
|
```` ```classname\ngithub-style\ncode blocks\n``` ````,
|
|
`<mail@example.com>`, `<http://example.com>` and
|
|
`[text](http://example.com "title")`.
|
|
> Ox.parseMarkdown('*foo* **bar** `baz` ``back`tick``')
|
|
'<em>foo</em> <strong>bar</strong> <code>baz</code> <code>back`tick</code>'
|
|
> Ox.parseMarkdown('foo\n\nbar\n\nbaz')
|
|
'foo<br><br>bar<br><br>baz'
|
|
> Ox.parseMarkdown('```foo\n\nbar\n\nbaz\n```')
|
|
'<pre><code class="foo">bar\n\nbaz\n</code></pre>'
|
|
> Ox.parseMarkdown('<http://example.com>')
|
|
'<a href="http://example.com">http://example.com</a>'
|
|
> Ox.parseMarkdown('`<http://example.com>`')
|
|
'<code><http://example.com></code>'
|
|
> Ox.parseMarkdown('[example](http://example.com "example.com")')
|
|
'<a href="http://example.com" title="example.com">example</a>'
|
|
> Ox.parseMarkdown('[example](http://example.com?foo=bar&bar=baz)')
|
|
'<a href="http://example.com?foo=bar&bar=baz">example</a>'
|
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).startsWith('<a href="')
|
|
true
|
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).endsWith('</a>')
|
|
true
|
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).count(':')
|
|
1
|
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).decodeHTMLEntities()
|
|
'<a href="mailto:mail@example.com">mail@example.com</a>'
|
|
*/
|
|
Ox.parseMarkdown = function(string) {
|
|
// see https://github.com/coreyti/showdown/blob/master/src/showdown.js
|
|
var array = [];
|
|
return string.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
.replace(
|
|
/(?:^|\n)```(.*)\n([^`]+)\n```/g,
|
|
function(match, classname, code) {
|
|
array.push(
|
|
'<pre><code'
|
|
+ (classname ? ' class="' + classname + '"' : '') + '>'
|
|
+ code.trim().replace(/</g, '<') + '\n</code></pre>'
|
|
);
|
|
return salt.join(array.length - 1);
|
|
}
|
|
)
|
|
.replace(
|
|
/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
|
|
function(match, prev, backticks, code, next) {
|
|
array.push(
|
|
prev + '<code>'
|
|
+ code.trim().replace(/</g, '<') + '</code>'
|
|
);
|
|
return salt.join(array.length - 1);
|
|
}
|
|
)
|
|
.replace(
|
|
/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,
|
|
'<strong>$2</strong>'
|
|
)
|
|
.replace(
|
|
/(\*|_)(?=\S)([^\r]*?\S)\1/g,
|
|
'<em>$2</em>'
|
|
)
|
|
.replace(
|
|
/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
|
|
function(match, all, text, id, url, rest, quote, title) {
|
|
return '<a href="' + Ox.encodeHTMLEntities(url) + '"' + (
|
|
title ? ' title="' + Ox.encodeHTMLEntities(title) + '"' : ''
|
|
) + '>' + text + '</a>';
|
|
}
|
|
)
|
|
.replace(
|
|
/<((https?|ftp|dict):[^'">\s]+)>/gi,
|
|
'<a href=\"$1\">$1</a>'
|
|
)
|
|
.replace(
|
|
/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
|
|
function(match, mail) {
|
|
return Ox.encodeEmailAddress(mail);
|
|
}
|
|
)
|
|
.replace(/\n\n/g, '<br><br>')
|
|
.replace(
|
|
new RegExp(salt.join('(\\d+)'), 'g'),
|
|
function(match, index) {
|
|
return array[parseInt(index)];
|
|
}
|
|
);
|
|
};
|
|
|
|
/*@
|
|
Ox.sanitizeHTML <f> Takes untrusted HTML and returns something trustworthy
|
|
> Ox.sanitizeHTML('http://foo.com, ...')
|
|
'<a href="http://foo.com">http://foo.com</a>, ...'
|
|
> Ox.sanitizeHTML('http://foo.com/foo?bar&baz, ...')
|
|
'<a href="http://foo.com/foo?bar&baz">http://foo.com/foo?bar&baz</a>, ...'
|
|
> Ox.sanitizeHTML('(see: www.foo.com)')
|
|
'(see: <a href="http://www.foo.com">www.foo.com</a>)'
|
|
> Ox.sanitizeHTML('foo@bar.com')
|
|
'<a href="mailto:foo@bar.com">foo@bar.com</a>'
|
|
> Ox.sanitizeHTML('<a href="mailto:foo@bar.com">foo</a>')
|
|
'<a href="mailto:foo@bar.com">foo</a>'
|
|
> Ox.sanitizeHTML('<a href="http://foo.com">foo</a>')
|
|
'<a href="http://foo.com">foo</a>'
|
|
> Ox.sanitizeHTML('<a href="http://www.foo.com/">http://www.foo.com/</a>')
|
|
'<a href="http://www.foo.com/">http://www.foo.com/</a>'
|
|
> Ox.sanitizeHTML('<a href="http://foo.com" onclick="alert()">foo</a>')
|
|
'<a href="http://foo.com">foo</a>'
|
|
> Ox.sanitizeHTML('<a href="javascript:alert()">foo</a>')
|
|
'<a href="javascript:alert()">foo'
|
|
> Ox.sanitizeHTML('<a href="foo">foo</a>')
|
|
'<a href="foo">foo'
|
|
> Ox.sanitizeHTML('<a href="/foo">foo</a>')
|
|
'<a href="/foo">foo</a>'
|
|
> Ox.sanitizeHTML('[http://foo.com foo]')
|
|
'<a href="http://foo.com">foo</a>'
|
|
> Ox.sanitizeHTML('<rtl>foo</rtl>')
|
|
'<div style="direction: rtl">foo</div>'
|
|
> Ox.sanitizeHTML('<script>alert()</script>')
|
|
'<script>alert()</script>'
|
|
> Ox.sanitizeHTML('\'foo\' < \'bar\' && "foo" > "bar"')
|
|
'\'foo\' < \'bar\' && "foo" > "bar"'
|
|
> Ox.sanitizeHTML('<b>foo')
|
|
'<b>foo</b>'
|
|
> Ox.sanitizeHTML('<b>foo</b></b>')
|
|
'<b>foo</b>'
|
|
> Ox.sanitizeHTML('&&')
|
|
'&&'
|
|
> Ox.sanitizeHTML('<http://foo.com>')
|
|
'<<a href="http://foo.com">http://foo.com</a>>'
|
|
@*/
|
|
Ox.sanitizeHTML = function(html, tags) {
|
|
var matches = [];
|
|
tags = tags || defaultTags;
|
|
// html = Ox.clean(html); fixme: can this be a parameter?
|
|
if (tags.indexOf('[]') > -1) {
|
|
html = html.replace(
|
|
/\[((\/|https?:\/\/|mailto:).+?) (.+?)\]/gi,
|
|
'<a href="$1">$3</a>'
|
|
);
|
|
tags = tags.filter(function(tag) {
|
|
return tag != '[]';
|
|
});
|
|
}
|
|
tags.forEach(function(tag) {
|
|
var array = replace[tag] || replace['*'](tag);
|
|
Ox.forEach(array, function(value) {
|
|
html = html.replace(value[0], function() {
|
|
matches.push(Ox.formatString(value[1], arguments));
|
|
return salt.join(matches.length - 1);
|
|
});
|
|
});
|
|
});
|
|
html = Ox.encodeHTMLEntities(Ox.decodeHTMLEntities(html));
|
|
matches.forEach(function(match, i) {
|
|
html = html.replace(new RegExp(salt.join(i)), match);
|
|
});
|
|
html = Ox.addLinks(html, true);
|
|
html = html.replace(/\n\n/g, '<br/><br/>');
|
|
// Close extra opening and remove extra closing tags.
|
|
// Note: this converts ''' to "'" and '"' to '"'
|
|
return Ox.normalizeHTML(html);
|
|
};
|
|
|
|
/*@
|
|
Ox.stripTags <f> Strips HTML tags from a string
|
|
> Ox.stripTags('f<span>o</span>o')
|
|
'foo'
|
|
@*/
|
|
Ox.stripTags = function(string) {
|
|
return string.replace(/<.*?>/g, '');
|
|
};
|
|
|
|
}());
|