Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

193 lines
5.9 KiB

'use strict';
const escapeRegExp = require('lodash.escaperegexp');
const capitalize = require('lodash.capitalize');
const isString = require('lodash.isstring');
const isPlainObject = require('lodash.isplainobject');
const uniqBy = require('lodash.uniqby');
const hostConfig = require('./lib/hosts-config');
const {hasOwnProperty} = Object.prototype;
const FENCE_BLOCK_REGEXP = /^(([ \t]*`{3,4})([^\n]*)([\s\S]+?)(^[ \t]*\2))/gm;
const CODE_BLOCK_REGEXP = /(`(?![\\]))((?:.(?!\1(?![\\])))*.?)\1/g;
const HTML_CODE_BLOCK_REGEXP = /(<code)+?((?!(<code|<\/code>)+?)[\S\s])*(<\/code>)+?/gim;
const LEADING_TRAILING_SLASH_REGEXP = /^\/?([^/]+(?:\/[^/]+)*)\/?$/;
const TRAILING_SLASH_REGEXP = /\/?$/;
function inverse(str) {
return str
.split('')
.reverse()
.join('');
}
function join(keywords) {
return keywords
.filter(Boolean)
.map(escapeRegExp)
.join('|');
}
function addLeadingAndTrailingSlash(value) {
return value.replace(LEADING_TRAILING_SLASH_REGEXP, '/$1/');
}
function addTrailingSlash(value) {
return value.replace(TRAILING_SLASH_REGEXP, '/');
}
function includesIgnoreCase(arr, value) {
return arr.findIndex(val => val.toUpperCase() === value.toUpperCase()) > -1;
}
function buildMentionsRegexp({mentionsPrefixes}) {
return `((?:(?:[^\\w\\n\\v\\r]|^)+(?:${join(mentionsPrefixes)})[\\w-\\.]+[^\\W])+)`;
}
function buildRefRegexp({actions, issuePrefixes, issueURLSegments, hosts}) {
return `(?:(?:[^\\w\\n\\v\\r]|^)+(${join([].concat(...Object.keys(actions).map(key => actions[key])))}))?(?:${[
'[^\\w\\n\\v\\r]|^',
join([...issuePrefixes, ...issueURLSegments]),
].join('|')})+${hosts.length > 0 ? `(?:${join(hosts)})?` : ''}((?:(?:[\\w-\\.]+)\\/)+(?:[\\w-\\.]+))?(${join([
...issuePrefixes,
...issueURLSegments,
])})(\\d+)(?!\\w)`;
}
function buildRegexp(opts) {
return new RegExp(
opts.mentionsPrefixes.length > 0
? `(?:${buildRefRegexp(opts)}|${buildMentionsRegexp(opts)})`
: buildMentionsRegexp(opts),
'gim'
);
}
function buildMentionRegexp({mentionsPrefixes}) {
return new RegExp(`(${join(mentionsPrefixes)})([\\w-.]+)`, 'gim');
}
function parse(text, regexp, mentionRegexp, {actions, issuePrefixes, hosts}) {
let parsed;
const results = {
actions: Object.keys(actions).reduce(
(result, key) => (actions[key].length > 0 ? Object.assign(result, {[key]: []}) : result),
{}
),
refs: [],
mentions: [],
};
let noCodeBlock = inverse(inverse(text.replace(FENCE_BLOCK_REGEXP, '')).replace(CODE_BLOCK_REGEXP, ''));
while (regexp.test(noCodeBlock)) {
noCodeBlock = noCodeBlock.replace(HTML_CODE_BLOCK_REGEXP, '');
}
while ((parsed = regexp.exec(noCodeBlock)) !== null) {
let [raw, action, slug, prefix, issue, mentions] = parsed;
prefix =
prefix && issuePrefixes.some(issuePrefix => issuePrefix.toUpperCase() === prefix.toUpperCase())
? prefix
: undefined;
raw = parsed[0].substring(
parsed[0].indexOf(
parsed[1] || hosts.find(host => parsed[0].toUpperCase().includes(host.toUpperCase())) || parsed[2] || parsed[3]
)
);
action = capitalize(parsed[1]);
const actionTypes = Object.keys(actions).filter(key => includesIgnoreCase(actions[key], action));
if (actionTypes.length > 0) {
for (const actionType of actionTypes) {
results.actions[actionType].push({raw, action, slug, prefix, issue});
}
} else if (issue) {
results.refs.push({raw, slug, prefix, issue});
} else if (mentions) {
let parsedMention;
while ((parsedMention = mentionRegexp.exec(mentions)) !== null) {
const [rawMention, prefixMention, user] = parsedMention;
results.mentions.push({raw: rawMention.trim(), prefix: prefixMention, user});
}
}
}
return results;
}
function typeError(parentOpt, opt) {
return new TypeError(
`The ${[parentOpt, opt].filter(Boolean).join('.')} property must be a String or an array of Strings`
);
}
function normalize(opts, parentOpt) {
for (const opt of Object.keys(opts)) {
if (!parentOpt && opt === 'actions') {
normalize(opts[opt], opt);
} else {
if (!opts[opt]) {
opts[opt] = [];
} else if (isString(opts[opt])) {
opts[opt] = [opts[opt]];
} else if (!Array.isArray(opts[opt])) {
throw typeError(parentOpt, opt);
}
if (opts[opt].length !== 0 && !opts[opt].every(opt => isString(opt))) {
throw typeError(parentOpt, opt);
}
opts[opt] = opts[opt].filter(Boolean);
}
}
}
module.exports = (options = 'default', overrides = {}) => {
if (!isString(options) && !isPlainObject(options)) {
throw new TypeError('The options argument must be a String or an Object');
}
if (isPlainObject(options) && hasOwnProperty.call(options, 'actions') && !isPlainObject(options.actions)) {
throw new TypeError('The options.actions property must be an Object');
}
if (isString(options) && !includesIgnoreCase(Object.keys(hostConfig), options)) {
throw new TypeError(`The supported configuration are [${Object.keys(hostConfig).join(', ')}], got '${options}'`);
}
if (!isPlainObject(overrides)) {
throw new TypeError('The overrides argument must be an Object');
} else if (hasOwnProperty.call(overrides, 'actions') && !isPlainObject(overrides.actions)) {
throw new TypeError('The overrides.actions property must be an Object');
}
options = isString(options) ? hostConfig[options.toLowerCase()] : options;
const opts = Object.assign({}, hostConfig.default, options, overrides, {
actions: Object.assign({}, hostConfig.default.actions, options.actions, overrides.actions),
});
normalize(opts);
opts.hosts = opts.hosts.map(addTrailingSlash);
opts.issueURLSegments = opts.issueURLSegments.map(addLeadingAndTrailingSlash);
const regexp = buildRegexp(opts);
const mentionRegexp = buildMentionRegexp(opts);
return text => {
if (!isString(text)) {
throw new TypeError('The issue text must be a String');
}
const results = parse(text, regexp, mentionRegexp, opts);
Reflect.defineProperty(results, 'allRefs', {
get() {
return uniqBy(this.refs.concat(...Object.keys(this.actions).map(key => this.actions[key])), 'raw');
},
});
return results;
};
};