VulpesOS/gaia/apps/sms/views/conversation/js/link_helper.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

301 lines
9.7 KiB
JavaScript
Executable file

/*global Utils */
(function() {
'use strict';
/*
The LinkHelper exposes functionality to apply the regexp
on the input strings and replace them with corresponding
anchor links for url, phone, email.
*/
var KNOWN_TLDS = [
'aero', 'asia', 'biz', 'cat', 'com', 'coop', 'info',
'int', 'jobs', 'mobi', 'museum', 'name', 'net', 'org', 'post', 'pro',
'tel', 'travel', 'xxx', 'edu', 'gov', 'mil', 'nyc', 'ac',
'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar',
'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf',
'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
'no', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci',
'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv', 'cx', 'cy',
'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg',
'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr',
'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn',
'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn',
'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir',
'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km',
'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk',
'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg',
'mh', 'mk', 'ml', 'mm', 'mn', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms',
'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe',
'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw',
'py', 'qa', 're', 'ro', 'rs', 'ru', 'su', 'рф', 'rw', 'sa', 'sb',
'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'no', 'sk', 'sl', 'sm',
'sn', 'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc',
'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tp', 'tm', 'tn', 'to',
'tp', 'tl', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'us',
'gov', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw'
];
var ipv4part = '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
var ipv4RegExp = new RegExp('^(?:' + ipv4part + '\\.){3}' + ipv4part + '$');
var leadingDigitsRegExp = /^[+0-9]+/;
// ensure that each part of the domain is long enough
function checkDomain(domain) {
// Check for a specific IPv4 address
if (ipv4RegExp.test(domain)) {
return true;
} else {
// Don't add many restrictions,
// just the tld to be non numeric and length > 1
var parts = domain.split('.');
var lastPart = parts[parts.length - 1];
// We want the last part not to be a number
return lastPart.length > 1 && !isFinite(lastPart);
}
}
// defines things that can match right before to be a "safe" link
var safeStart = /[\s.,:;\(>\u0080-\uFFFF]/;
const MINIMUM_DIGITS_IN_PHONE_NUMBER = 5;
const LEADING_DIGIT_BREAKPOINT = 7;
/**
* For each category of links:
* The key is the link type
* order matters - defined first means higher priority if two matches equal
*
* regexp: The regular expression to match potential links
* matchFilter: (optional) A function that is passed the matched part, and
* the linkSpec.
* Should return the linkSpec, or false if not to be matched
* transform: A function that converts the match data to a html link string
* Is passed the matched part and the linkSpec.
*/
var LINK_TYPES = {
phone: {
regexp: new RegExp([
// sddp: space, dot, dash or parens
'(?:\\+\\d{1,4}[ \\t.()-]{0,3}|\\()?' + // (\+<digits><sddp>|\()?
'(?:\\d{1,4}[ \\t.()-]{0,3})?' + // <digits><sdd>*
'(?:\\d[\\d \\t.()-]{0,12}\\d)' + // <digit><digit|sddp>*<digit>
'\\b' // must end on a word boundary
].join(''), 'g'),
matchFilter: function phoneMatchFilter(phone, link) {
var onlyDigits = Utils.removeNonDialables(phone);
if (onlyDigits.length < MINIMUM_DIGITS_IN_PHONE_NUMBER) {
return false;
}
if (onlyDigits.length === MINIMUM_DIGITS_IN_PHONE_NUMBER &&
phone.length !== onlyDigits.length) {
return false;
}
var leadingDigits = leadingDigitsRegExp.exec(phone);
if (leadingDigits &&
leadingDigits[0].length >= LEADING_DIGIT_BREAKPOINT) {
link.end = link.start + leadingDigits[0].length;
}
return link;
},
transform: function phoneTransform(phone, link) {
return '<a dir="ltr" data-dial="' + phone +
'" data-action="dial-link">' + phone + '</a>';
}
},
url: {
regexp: new RegExp([
// must begin at start of string, after whitespace,
// {1} match the protocol https?:// or rtsp:// (optional)
'(https?://|rtsp://)?',
// {2} match "server name": . must be followed by at least one letter
'(\\w(?:\\.?[-\\w]){0,255})',
// {3} match a . followed by one or more domain valid chars
'(\\.\\w{1,10})',
// optional :port
'(?::[0-9]{1,5})?',
// start the "query" capture group by matching an optional dot then /
'(?:\\.?/',
// anything other than a sentence ender
// a sentence ender not followed by whitespace
// match 0 - 2048 characters in the url
'(?:[^\\s.,?:;!]|[.,?:;!](?!\\s|$)){0,2048}',
// end the "query" capture group (optional)
')?'
].join(''), 'mgi'),
matchFilter: function urlMatchFilter(url, linkSpec) {
var match = linkSpec.match;
var scheme, tld;
if (!checkDomain(match[2] + match[3])) {
return false;
}
scheme = match[1];
tld = match[3] && match[3].slice(1);
// For Cases where:
//
// 1. There was no scheme (eg, "http", "https", "rtsp")
// 2. The matched tld is not a number
// 3. The matched tld is not a known tld
//
// ... Do not create a clickable link
if (!scheme && (!isFinite(tld) && KNOWN_TLDS.indexOf(tld) === -1)) {
return false;
}
// strip a trailing ) if there isn't a ( in the url
if (url.slice(-1) === ')' && url.indexOf('(') === -1) {
linkSpec.end--;
}
return linkSpec;
},
transform: function urlTransform(url, linkSpec) {
var href = url;
if (!linkSpec.match[1]) {
href = 'http://' + href;
}
return '<a data-url="' + href + '" data-action="url-link" >' + url +
'</a>';
}
},
email: {
regexp: /[\w.+-]+@[\w.-]+\.[a-z.]{2,6}/mgi,
transform: function emailTransform(email) {
return [
'<a data-email="',
'" data-action="email-link">',
'</a>'
].join(email);
}
}
};
var LINK_TYPES_KEYS = Object.keys(LINK_TYPES);
function searchForLinks(type, string) {
var linkSpecs = [];
var spec = LINK_TYPES[type];
if (!spec) {
return linkSpecs;
}
var regexp = spec.regexp;
var matchFilter = spec.matchFilter;
var match, linkSpec;
// while we match stuff...
while ((match = regexp.exec(string))) {
// if the match isn't at the begining of the string, check for a safe
// character before the match
var rest = string.slice(match.index - 1);
if (match.index && !safeStart.test(rest.charAt(0))) {
// we should only advance the regexp to the next safeStart
var nextSafe = safeStart.exec(rest);
if (nextSafe) {
regexp.lastIndex = match.index + nextSafe.index;
}
continue;
}
linkSpec = {
type: type,
start: match.index,
length: match[0].length,
end: match.index + match[0].length,
match: match
};
if (matchFilter) {
linkSpec = matchFilter(match[0], linkSpec);
}
if (linkSpec) {
linkSpecs.push(linkSpec);
regexp.lastIndex = linkSpec.end;
}
}
return linkSpecs;
}
function linkSort(a, b) {
// sort by starting position first
// then by ending position reversed
// then by the order of the type in the LINK_TYPES
return (a.start - b.start) ||
(b.end - a.end) ||
(LINK_TYPES_KEYS.indexOf(a.type) - LINK_TYPES_KEYS.indexOf(b.type));
}
function removeOverlapping(linkSpecs) {
linkSpecs.sort(linkSort);
for (var index = 0; index < linkSpecs.length - 1; index++) {
var end = linkSpecs[index].end;
// while there are more linkSpecs, and they start before we end remove the
// overlapping matches
while (linkSpecs[index + 1] && linkSpecs[index + 1].start < end) {
linkSpecs.splice(index + 1, 1);
}
}
}
var LinkHelper = window.LinkHelper = {
searchAndLinkUrl: function lh_searchAndLinkUrl(inputText) {
return LinkHelper.searchAndLinkClickableData(inputText, { url: true });
},
searchAndLinkEmail: function lh_searchAndLinkEmail(inputText) {
return LinkHelper.searchAndLinkClickableData(inputText, { email: true });
},
searchAndLinkPhone: function lh_searchAndLinkPhone(inputText) {
return LinkHelper.searchAndLinkClickableData(inputText, { phone: true });
},
// Invokes resepective functions to change url, phone and email strings
// and make them active links
// |inputText| should be already html escaped
searchAndLinkClickableData:
function lh_searchAndLinkClickableData(inputText, mode) {
// default is everything enabled
mode = mode || LINK_TYPES;
var linkSpecs = [];
var type;
for (type in mode) {
if (mode[type] && LINK_TYPES[type]) {
linkSpecs = linkSpecs.concat(searchForLinks(type, inputText));
}
}
removeOverlapping(linkSpecs);
var result = '';
var lastEnd = 0;
linkSpecs.forEach(function replaceLink(link) {
result += inputText.slice(lastEnd, link.start);
var replacing = inputText.slice(link.start, link.end);
var replaceWith = LINK_TYPES[link.type].transform(replacing, link);
result += replaceWith;
lastEnd = link.end;
});
result += inputText.slice(lastEnd);
return result;
}
};
})();