glipdown
Version:
Glip-flavored markdown
451 lines (403 loc) • 14.2 kB
JavaScript
/*globals window, Client */
var TLDS = TLDS || [];
if (typeof require != 'undefined') {
TLDS = require('./tlds').TLDS;
}
function loadValidTLDS() {
var tldsArray = [];
TLDS.forEach(function(tld){
tldsArray.push('\\.' + tld + '(?!\\.|\\w)');
});
return tldsArray.join('|');
}
var validLinkMarkDownRegEx = new RegExp('[a-zA-Z0-9](' + loadValidTLDS() + ')|^https?:\\/\\/[a-zA-Z0-9]', "i");
var _ = _ || null;
if ((typeof require != 'undefined') && !_) {
var _ = require('underscore');
}
var anchor_tag_regexp = new RegExp('<a.*?>.*?<\/a>', 'g');
var tokenized_anchor_tag_regexp = new RegExp('{{-{{a.*?}}-}}.*?{{-{{\/a}}-}}', 'g');
var escaped_anchor_tag_regexp = new RegExp('&(?:amp;){0,2}lt;a.*?&(?:amp;){0,2}gt;.*?&(?:amp;){0,2}lt;(?:\/|&(?:amp;){0,2}#x2F;)a&(?:amp;){0,2}gt;', 'g');
var any_tag_regexp = new RegExp('<[a-z].*?>', 'g');
var tokenized_any_tag_regexp = new RegExp('{{-{{[a-z].*?}}-}}', 'g');
var escaped_any_tag_regexp = new RegExp('&(?:amp;){0,2}lt;[a-z].*?&(?:amp;){0,2}?gt;', 'g');
function build_location_cache(cache, raw_string, match_pattern_array) {
var location_cache;
var string_to_match = typeof raw_string === 'string' ? raw_string : '';
var regexp_array = Array.isArray(match_pattern_array) ? match_pattern_array : [];
if (cache && cache.built) {
return cache;
};
location_cache = {
built: true,
locations: []
};
regexp_array.forEach(function(match_pattern) {
var index = 0;
var position;
var match_regexp = match_pattern instanceof RegExp ? match_pattern : new RegExp();
var matches = string_to_match.match(match_regexp) || [];
var matched_tag_locations = {};
for (index = 0; index < matches.length; index++) {
current_match = matches[index];
position = string_to_match.indexOf(
current_match,
matched_tag_locations[current_match] || 0
);
matched_tag_locations[current_match] = position + 1;
location_cache.locations.push({
start: position,
end: position + current_match.length
});
}
});
return location_cache;
}
function is_in_location_cache(cache, location_to_check) {
var found_in_cache;
var location_object_array;
var array_length;
var location_object;
if (!cache ||
!cache.locations ||
!Array.isArray(cache.locations) ||
typeof location_to_check !== 'number'
) {
return false;
}
location_object_array = cache.locations.slice().reverse();
array_length = location_object_array.length;
while(array_length && !found_in_cache) {
location_object = location_object_array[array_length -1];
found_in_cache = location_to_check > location_object.start &&
location_to_check < location_object.end;
array_length -= 1;
}
return found_in_cache;
}
var Markdown = function(raw, options) {
var options = options || {};
var phone_util;
var is_valid_pstn = function () {
return false;
};
var team_join_link_regexp;
if (!raw) {
return '';
}
if (!options.dont_escape) {
raw = _.escape(raw);
}
if (typeof Client !== 'undefined') {
phone_util = Client.get_controller('Phone_Number');
is_valid_pstn = phone_util && typeof phone_util.is_valid_pstn === 'function' ?
phone_util.is_valid_pstn :
is_valid_pstn;
}
var code_blocks = {};
var block_count = 0;
var quote_tag = options.use_blockquote ? 'span class="blockquote"' : 'q';
var end_quote_tag = options.use_blockquote ? '/span' : '/q';
var link_last_offset = 0;
var link_was_inside_tag = 0;
var link_array = [];
var score_array= [];
var timestamp_array = [];
var anchor_tag_location_cache;
if (typeof window !== 'undefined' && window.location && window.location.origin) {
team_join_link_regexp = new RegExp('^' + window.location.origin + '/r/join/');
}
var val = raw.replace(/\&\#x2F;/g, '/') // not sure why underscore replaces these...docs don't even claim that it does
.replace(/\\]|\\\)/g, function (match) {
return {
'\\]': '%5D',
'\\\)': '%29'
}[match]
})
.replace(/\[([^\]]*?)]\(([^)]*?)\)/g, function(full_match, text, link) {
var link_pieces;
var group_id;
text = String(text).replace(/%5D/g, ']');
link = String(link).replace(/'%29/g, ')');
if (text === 'code' || !validLinkMarkDownRegEx.test(link)) {
return full_match;
}
text = text.replace(/\\([\[\]()])/g, '$1');
link_pieces = link.split('/');
group_id = link_pieces[link_pieces.length - 1];
return team_join_link_regexp && link.match(team_join_link_regexp) && link.indexOf('join') !== 0 ?
"<div onclick='Router.join_from_url(" + group_id + ", true)' class='team_join_link'>" + link + "</div>" :
"<a href='" + link + "' target='_blank' rel='noreferrer'>" + text + "</a>";
})
.replace(/%5D|%29/g, function (match) { //for the rare cases of escaped brackets happening outside of links
return {
'%5D': '\\]',
'%29': '\\\)'
}[match]
})
.replace(/((^|\n)> ([^\n]*))+\n?/g, function(full_match) {
return "<" + quote_tag + ">\n" + full_match.trim().replace(/^> /gm, '') + "\n<" + end_quote_tag + ">";
})
.replace(/\[code\]([\s\S]*?)(\[\/code\]|$)/gi, function(full_match, text) {
var code;
try {
code = unescape(text);
code = code.replace(/</g, '<').replace(/>/g, '>');
}
catch (error) {
code = text.replace(/(\%\w\w)/g, function (fm, esc) {
return unescape(esc);
});
}
code_blocks['code_' + block_count] = code;
var to_return = '[code_' + block_count + ']';
block_count++;
return to_return;
})
.replace(Markdown.global_url_regex, function(
full_match,
maybe_email1,
maybe_email2,
link,
proto_and_slashes,
protocol,
junk,
junkpoint5,
junk1,
junk2,
junk3,
junk4,
junk5,
junk6,
last_char,
offset,
full_str
) {
var link_pieces;
var group_id;
var link_token = '{{--LINK-' + link_array.length + '--}}';
var include_space = full_match.match(/ $/);
if (!validLinkMarkDownRegEx.test(link)) {
return full_match;
}
var start = full_str.substr(link_last_offset, offset - link_last_offset);
link_last_offset = offset;
var inside_tag = link_was_inside_tag ? (start.match(/.*>[^<]*$/) ? false : true) : start.match(/.*<[^>]*$/);
if (inside_tag) {
link_was_inside_tag = true;
return full_match;
}
link_was_inside_tag = false;
var inside_post_tag = start.match(/.*(\{\{-\{\{)(?![\s\S]*\}\}-\}\})[\s\S]*$/);
if (inside_post_tag) {
return full_match;
}
var lead_match = full_str.substring(0, offset);
var lead_str = lead_match + full_match;
var link_matches = lead_str.replace(/\{\{\-\{\{/g, '<').replace(/\}\}-\}\}/g, '>').match(/^[\s\S]*<a[\s\S]*?>([\s\S]*?)$/);
if (link_matches) {
var link_match = link_matches[1];
if (!link_match.match(/<\/a>/) || link_match.match(/<\/a>$/)) {
return full_match;
}
}
//console.warn("nomatch:", +new Date() - sub_bench);
if (team_join_link_regexp && link.match(team_join_link_regexp) && link.indexOf('join') !== 0) {
link_pieces = link.split('/');
group_id = link_pieces[link_pieces.length - 1];
link_array.push("<div onclick='Router.join_from_url(" + group_id + ", true)' class='team_join_link'>" + link + "</div>");
return link_token;
}
// ensures that the characters ".,?:;()*&!" will not become
// apart of a link if they are placed at the end of one
link = link.replace('&', '&');
link = link.replace(/[.,?:;()*&!~]$/, function (m) {
last_char = m + (include_space ? ' ' : '');
return '';
});
link_array.push("<a href='" +
(
maybe_email2 && !protocol ? 'mailto:' + maybe_email2 :
(protocol ? '' : 'http://')
) + link + "' target='_blank' rel='noreferrer'>" +
(maybe_email2 ? maybe_email2 : '') +
link + "</a>");
return link_token + last_char;
})
.replace(/\|([^\n]*)\|(\s|\n|$)/g, function(full_match, text) {
var cols = text.split(/\|/);
if (!cols.length) {
return text;
}
var percent = (100 / cols.length).toFixed(0);
return "<table><tr valign='top'><td width='" + percent + "%'>" +
cols.join("</td><td width='" + percent + "%'>") +
"</tr></table>";
})
.replace(/<\/table><table>/g, '')
.replace(/((^|\n)\* [^\n]*)+/g, function(full_match) {
var parts = full_match.split(/\n/);
if (parts[0].length === 0) {
parts.shift();
}
return "<ul><li>" +
parts.map(function (part) { return part.replace(/^\* /, ''); }).
join("</li><li>") +
"</li></ul>";
})
.replace(/((^|\n)\d+\. [^\n]*)+/g, function (full_match) {
var parts = full_match.split(/\n/);
var start = parseInt(full_match, 10);
var start_text = "";
if (parts[0].length === 0) {
parts.shift();
}
if (!isNaN(start) && start > 1) {
start_text = " start=" + start + " style='counter-reset: li " + (start - 1) + "'";
}
return "<ol" + start_text +"><li>" +
parts.map(function (part) {
return part.replace(/^\d+. /, '');
}).join("</li><li>") +
"</li></ol>";
}).replace(new RegExp("<" + quote_tag + ">((?:.|\\n)+)<" + end_quote_tag + ">", 'gm'), function(full_match, p1) {
return "<" + quote_tag + ">" + p1.trim() + "<" + end_quote_tag + ">";
})
.replace(/<\/([ou])l>\n/g, '</$1l>')
.replace(/\*\*([^\*\*]+)\*\*/g, function (full_match, text) {
return "<b>" + text + "</b>";
})
.replace(/\*([^\*]+)\*/g, function (full_match, text) {
return "<i>" + text + "</i>";
})
.replace(/__([^__]+)__/g, function (full_match, text) {
return "<u>" + text + "</u>";
})
.replace(/~~([^~~]+)~~/g, function (full_match, text) {
return "<strike>" + text + "</strike>";
})
.replace(/\{\{\[\[-([^\{\{\[\[]*?)-\]\]\}\}/g, function(full_match, text) {
return "<span class='search_match_stream'>" + text + "</span>";
})
.replace(/\[code_(\w+)\]/g, function(full_match, which) {
return "<pre class=codesnippet>" + code_blocks['code_' + which] + "</pre>";
})
.replace(Markdown.timestamp_regex, function skip_timestamps(
match,
offset,
full_str) {
var time_token = '{{--TIME-' + timestamp_array.length + '--}}';
timestamp_array.push(match);
return time_token;
})
.replace(Markdown.score_regex, function skip_score(
match,
offset,
full_str) {
var score_token = '{{--SCORE-' + score_array.length + '--}}';
score_array.push(match);
return score_token;
})
.replace(Markdown.potential_phone_regex, function mark_phone_numbers(
match,
offset,
full_str
) {
var in_anchor_tag;
var is_valid_number = match !== '911' && match !== '999' && is_valid_pstn(match);
var regexp_to_check;
if (!is_valid_number) {
return match;
}
regexp_to_check = [
anchor_tag_regexp,
tokenized_anchor_tag_regexp,
escaped_anchor_tag_regexp,
any_tag_regexp,
tokenized_any_tag_regexp,
escaped_any_tag_regexp
];
anchor_tag_location_cache = build_location_cache(
anchor_tag_location_cache,
full_str,
regexp_to_check
);
in_anchor_tag = is_in_location_cache(anchor_tag_location_cache, offset);
return !in_anchor_tag ?
"<a href='tel:" + match + "' class='markdown_phone_number'>" + match + '</a>' :
match;
})
.replace(/{{--LINK-(\d*)--}}/g, function (link_token, link_index) {
return link_array[link_index] || link_token;
})
.replace(/mailto:<a href=/g, function (full_match, which) {
return "<a href=";
})
.replace(/{{--TIME-(\d*)--}}/g, function(time_token, time_index) {
return timestamp_array[time_index] || time_token;
})
.replace(/{{--SCORE-(\d*)--}}/g, function(score_token, score_index) {
return score_array[score_index] || score_token;
});
return val;
};
var Markdown_Is_Valid_Link = function (link) {
return validLinkMarkDownRegEx.test(link);
};
Markdown.tld_url_regex = validLinkMarkDownRegEx;
Markdown.url_regex = /^((ftp|https?):\/\/)?[-\w]+\.([-\w]+\.)*(\d+\.\d+\.\d+|[-A-Za-z]+)(:\d+)?($|(\/\S?(\/\S)*\/?)|(\#\S?)|(\?\S?))/i;
Markdown.global_url_regex = /(([a-zA-Z0-9\!\#\$\%\&\'\*\+\-\/\=\?\%\_\`\{\|\}\~\.]+@)?)(((ftp|https?):\/\/)?[-\w]+\.([-\w]+\.)*(\d+\.\d+\.\d+|[-A-Za-z]+)(:\d+)?(((\/([A-Za-z0-9-\._~:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=])*)+)\??([A-Za-z0-9-\._~:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\%])*)?)([^A-Za-z]|$)/gi;
Markdown.score_regex = /(?:(\s|\n|^|,))\d{1,2}-(\d{1,2}(?!\d|-))/g;
Markdown.timestamp_regex = /\d\d\d\d(?:[-, ])[01]\d(?:[-, ])[0-3]\d [0-2]\d:\d\d:[0-5]\d/g;
Markdown.potential_phone_regex = /\+?(?:\d{1,4} ?)?(?:(?:\(\d{1,4}\)|\d(?:(?: |\-)?\d){0,3})(?:(?: |\-)?\d){2,}|(?:\(\d{2,4}\)|\d(?:(?: |\-)?\d){1,3})(?:(?: |\-)?\d){1,})(?:(?: x| ext.?)\d{1,5}){0,1}/g;
var Markdown_For_Search = function (raw, options) {
options = options || {};
if (!raw) { return ''; }
if (!options.dont_escape) {
raw = _.escape(raw);
}
return raw.replace(/\{\{\[\[-([^\{\{\[\[]*?)-\]\]\}\}/g, function(full_match, text) {
return "<span class='search_match_stream'>" + text + "</span>";
});
};
var Remove_Markdown = function (raw, options) {
options = options || {};
return (options.dont_escape ? raw : _.escape(raw)).
replace(/\&\#x2F;/g, '/'). // not sure why underscore replaces these...docs don't even claim that it does
replace(/\[code\]/g, '').
replace(/\[\/code\]/g, '').
replace(/\|([^\n]*)\|/g, function (full_match, text) {
var cols = text.split(/\|/);
if (!cols.length) {
return text;
}
return cols.join('\t');
}).
replace(/\*\*(\S[^\*\*]*?\S)\*\*/g, function (full_match, text) {
return text;
}).
// replace(/\`(.*?)\`/g, function(full_match, text) {
// return text;
// }).
replace(/\*(\S[^\*]*?\S)\*/g, function (full_match, text) {
return text;
}).
replace(/__(\S[^__]*?\S)__/g, function (full_match, text) {
return text;
}).
replace(/~~(\S[^~~]*?\S)~~/g, function (full_match, text) {
return text;
}).
replace(/(^|\n)> ([^\n]*)/g, function (full_match, start, text) {
return start + " " + text + " ";
}).
replace(/\[([^\]]*?)\]\(([\s\S]*?)\)/g, function (full_match, text, link) {
return text;
});
};
if (typeof exports == 'object') {
exports.Markdown = Markdown;
exports.Remove_Markdown = Remove_Markdown;
exports.Markdown_For_Search = Markdown_For_Search;
exports.Markdown_Is_Valid_Link = Markdown_Is_Valid_Link;
}