utilities.js

'use strict';

import {linkToken} from './builder';

const LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g;
const EMAIL_REGEX = /[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/gi;

const HOP = Object.prototype.hasOwnProperty;

export function has(object, key) {
	return object ? HOP.call(object, key) : false;
}

/*export function linkify(text, tokenFormatter) {
	LINK_REGEX.lastIndex = 0;
	const out = [];

	let idx = 0, match;
	while ((match = LINK_REGEX.exec(text))) {
		const nix = match.index;
		if ( idx !== nix )
			out.push(text.slice(idx, nix));

		let url = match[0];
		if ( url.endsWith(')') ) {
			let open = 1, i = url.length - 1;
			while (i--) {
				const chr = url[i];
				if ( chr === ')' )
					open++;
				else if ( chr === '(' )
					open--;

				if ( ! open )
					break;
			}

			if ( open )
				url = url.slice(0, url.length - 1);
		}

		let token = tokenFormatter
			? tokenFormatter(url, match)
			: linkToken(
				`${match[1] ? '' : 'https://'}${url}`,
				match[1] ? undefined : url
			);

		out.push(token);

		idx = nix + url.length;
	}

	if ( idx < text.length )
		out.push(text.slice(idx));

	if ( out.length === 1 )
		return out[0];

	return out;
}*/


export function linkifyEmail(tokens, tokenFormatter) {
	return linkifyMatching(tokens, EMAIL_REGEX, tokenFormatter ?? (match => {
		return linkToken(
			`mailto:${match[0]}`,
			match[0]
		);
	}));
}


export function linkify(tokens, tokenFormatter) {
	return linkifyMatching(tokens, LINK_REGEX, match => {
		let url = match[0];
		if ( url.endsWith(')') ) {
			let open = 1, i = url.length - 1;
			while (i--) {
				const chr = url[i];
				if ( chr === ')' )
					open++;
				else if ( chr === '(' )
					open--;

				if ( ! open )
					break;
			}

			if ( open )
				url = url.slice(0, url.length - 1);
		}

		if ( tokenFormatter )
			return tokenFormatter(url, match);

		return linkToken(
			`${match[1] ? '' : 'https://'}${url}`,
			match[1] ? undefined : url
		);
	});
}


export function linkifyMatching(tokens, regex, tokenFormatter, include) {
	const out = [];
	regex.lastIndex = 0;

	if ( ! Array.isArray(tokens) )
		tokens = [tokens];

	for(const text of tokens) {
		if ( typeof text !== 'string' ) {
			out.push(text);
			continue;
		}

		let idx = 0, match;
		while ((match = regex.exec(text))) {
			const token = tokenFormatter(match, out);
			if ( ! token )
				continue;

			const to_inc = include ? include(match, out) : 0;

			const nix = match.index + to_inc;
			if ( idx !== nix )
				out.push(text.slice(idx, nix));

			if ( Array.isArray(token) ) {
				for(const tok of token)
					out.push(tok);
			} else
				out.push(token);

			idx = nix + match[0].length - to_inc;
		}

		if ( idx === 0 )
			out.push(text);
		else if ( idx < text.length )
			out.push(text.slice(idx));

	}

	return out.length === 1
		? out[0]
		: out
}

/**
 * Truncate a string. Tries to intelligently break the string in white-space
 * if possible, without back-tracking. The returned string can be up to
 * `ellipsis.length + target + overage` characters long.
 * @param {String} str The string to truncate.
 * @param {Number} target The target length for the result
 * @param {Number} overage Accept up to this many additional characters for a better result
 * @param {String} [ellipsis='…'] The string to append when truncating
 * @param {Boolean} [break_line=true] If true, attempt to break at the first LF
 * @param {Boolean} [trim=true] If true, runs trim() on the string before truncating
 * @returns {String} The truncated string
 */
export function truncate(str, target = 100, overage = 15, ellipsis = '…', break_line = true, trim = true) {
	if ( ! str || ! str.length )
		return str;

	if ( trim )
		str = str.trim();

	let idx = break_line ? str.indexOf('\n') : -1;
	if ( idx === -1 || idx > target )
		idx = target;

	if ( str.length <= idx )
		return str;

	let out = str.slice(0, idx).trimRight();
	if ( overage > 0 && out.length >= idx ) {
		let next_space = str.slice(idx).search(/\s+/);
		if ( next_space === -1 && overage + idx > str.length )
			next_space = str.length - idx;

		if ( next_space !== -1 && next_space <= overage ) {
			if ( str.length <= (idx + next_space) )
				return str;

			out = str.slice(0, idx + next_space);
		}
	}

	return out + ellipsis;
}


const SIZE_UNITS = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const SIZE_FORMATTER = new Intl.NumberFormat('en-us', {maximumFractionDigits: 2});

/**
 * Format a file-size for readability.
 *
 * @param {Number} bytes The number of bytes
 * @returns {String} Formatted filesize.
 */
export function formatSize(bytes) {
	const sign = Math.sign(bytes) === -1 ? '-' : '';
	bytes = Math.abs(bytes);
	if ( bytes < 1000 )
		return `${sign}${bytes} B`;

	let u = -1;
	do {
		bytes /= 1000;
		++u;
	} while ( bytes >= 1000 && u < SIZE_UNITS.length - 1 );

	return `${sign}${SIZE_FORMATTER.format(bytes)} ${SIZE_UNITS[u]}`;
}


export function delimitArray(array, delimiter = ' • ') {
	const out = [];
	for (let i = 0, l = array.length; i < l; i++) {
		if ( i > 0 )
			out.push(delimiter);
		out.push(array[i]);
	}

	return out;
}