Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ jobs:
strategy:
matrix:
node-version:
- 18.17
- 20.6.1
- 20
- 21
- 22.22.2
- 24.15
- 24

runs-on: ubuntu-latest

Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22
24
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ $ npm install --save issue-parser
### GitHub format

```js
const issueParser = require('issue-parser');
import issueParser from 'issue-parser';
const parse = issueParser('github');

parse('Issue description, ref user/package#1, Fix #2, Duplicate of #3 /cc @user');
Expand All @@ -44,7 +44,7 @@ parse('Issue description, ref user/package#1, Fix #2, Duplicate of #3 /cc @user'
### GitLab format

```js
const issueParser = require('issue-parser');
import issueParser from 'issue-parser';
const parse = issueParser('gitlab');

parse('Issue description, ref group/user/package#1, !2, implement #3, /duplicate #4 /cc @user');
Expand All @@ -66,7 +66,7 @@ parse('Issue description, ref group/user/package#1, !2, implement #3, /duplicate
### Bitbucket format

```js
const issueParser = require('issue-parser');
import issueParser from 'issue-parser';
const parse = issueParser('bitbucket');

parse('Issue description, ref user/package#1, fixing #2. /cc @user');
Expand All @@ -84,7 +84,7 @@ parse('Issue description, ref user/package#1, fixing #2. /cc @user');
### Custom format

```js
const issueParser = require('issue-parser');
import issueParser from 'issue-parser';
const parse = issueParser({actions: {fix: ['complete'], hold: ['holds up']}, issuePrefixes: ['🐛']});

parse('Issue description, related to user/package🐛1, Complete 🐛2, holds up 🐛3');
Expand All @@ -102,7 +102,7 @@ parse('Issue description, related to user/package🐛1, Complete 🐛2, holds up
### Extend existing format

```js
const issueParser = require('issue-parser');
import issueParser from 'issue-parser';
const parse = issueParser('github', {actions: {parent: ['parent of'], related: ['related to']}});

parse('Issue description, ref user/package#1, Fix #2, Parent of #3, related to #4 /cc @user');
Expand Down Expand Up @@ -368,7 +368,7 @@ Option overrides. Useful when using predefined [`options`](#options) (such as `g

For example, the following will use all the `github` predefined options but with a different `hosts` option:
```js
const issueParser = require('issue-parser');
import issueParser from 'issue-parser';
const parse = issueParser('github', {hosts: ['https://custom-url.com']});
```

Expand Down
103 changes: 50 additions & 53 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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');
import escapeRegExp from "lodash.escaperegexp";
import capitalize from "lodash.capitalize";
import isString from "lodash.isstring";
import isPlainObject from "lodash.isplainobject";
import uniqBy from "lodash.uniqby";
import hostConfig from "./lib/hosts-config.js";

const {hasOwnProperty} = Object.prototype;
const { hasOwnProperty } = Object.prototype;

/* eslint prefer-named-capture-group: "off" */

Expand All @@ -17,40 +17,34 @@ const LEADING_TRAILING_SLASH_REGEXP = /^\/?([^/]+(?:\/[^/]+)*)\/?$/;
const TRAILING_SLASH_REGEXP = /\/?$/;

function inverse(string) {
return string
.split('')
.reverse()
.join('');
return string.split("").reverse().join("");
}

function join(keywords) {
return keywords
.filter(Boolean)
.map(escapeRegExp)
.join('|');
return keywords.filter(Boolean).map(escapeRegExp).join("|");
}

function addLeadingAndTrailingSlash(value) {
return value.replace(LEADING_TRAILING_SLASH_REGEXP, '/$1/');
return value.replace(LEADING_TRAILING_SLASH_REGEXP, "/$1/");
}

function addTrailingSlash(value) {
return value.replace(TRAILING_SLASH_REGEXP, '/');
return value.replace(TRAILING_SLASH_REGEXP, "/");
}

function includesIgnoreCase(array, value) {
return array.findIndex(arrayValue => arrayValue.toUpperCase() === value.toUpperCase()) > -1;
return array.findIndex((arrayValue) => arrayValue.toUpperCase() === value.toUpperCase()) > -1;
}

function buildMentionsRegexp({mentionsPrefixes}) {
function buildMentionsRegexp({ mentionsPrefixes }) {
return `((?:(?:[^\\w\\n\\v\\r]|^)+(?:${join(mentionsPrefixes)})[\\w-\\.]+[^\\W])+)`;
}

function buildRefRegexp({actions, delimiters, issuePrefixes, issueURLSegments, hosts}) {
function buildRefRegexp({ actions, delimiters, issuePrefixes, issueURLSegments, hosts }) {
return `(?:(?:[^\\w\\n\\v\\r]|^)+(${join(
Object.keys(actions).flatMap(key => actions[key])
)}))?(?:[^\\w\\n\\v\\r]|^|(?: |\\t)*(?:${join([' ', '\t', ...delimiters])})(?: |\\t)*)${
hosts.length > 0 ? `(?:${join(hosts)})?` : ''
Object.keys(actions).flatMap((key) => actions[key])
)}))?(?:[^\\w\\n\\v\\r]|^|(?: |\\t)*(?:${join([" ", "\t", ...delimiters])})(?: |\\t)*)${
hosts.length > 0 ? `(?:${join(hosts)})?` : ""
}((?:(?:[\\w-\\.]+)\\/)+(?:[\\w-\\.]+))?(${join([...issuePrefixes, ...issueURLSegments])})(\\d+)(?!\\w)`;
}

Expand All @@ -59,59 +53,62 @@ function buildRegexp(options) {
options.mentionsPrefixes.length > 0
? `(?:${buildRefRegexp(options)}|${buildMentionsRegexp(options)})`
: buildMentionsRegexp(options),
'gim'
"gim"
);
}

function buildMentionRegexp({mentionsPrefixes}) {
return new RegExp(`(${join(mentionsPrefixes)})([\\w-\\.]+)`, 'gim');
function buildMentionRegexp({ mentionsPrefixes }) {
return new RegExp(`(${join(mentionsPrefixes)})([\\w-\\.]+)`, "gim");
}

function parse(text, regexp, mentionRegexp, {actions, issuePrefixes, hosts}) {
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),
(result, key) => (actions[key].length > 0 ? Object.assign(result, { [key]: [] }) : result),
{}
),
refs: [],
mentions: [],
};
let filteredText = inverse(inverse(text.replace(FENCE_BLOCK_REGEXP, '')).replace(CODE_BLOCK_REGEXP, ''));
let filteredText = inverse(inverse(text.replace(FENCE_BLOCK_REGEXP, "")).replace(CODE_BLOCK_REGEXP, ""));

while (regexp.test(filteredText)) {
filteredText = filteredText.replace(HTML_CODE_BLOCK_REGEXP, '');
filteredText = filteredText.replace(HTML_CODE_BLOCK_REGEXP, "");
}

filteredText = filteredText.replace(HTML_COMMENT_REGEXP, ' ');
filteredText = filteredText.replace(HTML_COMMENT_REGEXP, " ");

while ((parsed = regexp.exec(filteredText)) !== null) {
let [raw, action, slug, prefix, issue, mentions] = parsed;
prefix =
prefix && issuePrefixes.some(issuePrefix => issuePrefix.toUpperCase() === prefix.toUpperCase())
prefix && issuePrefixes.some((issuePrefix) => issuePrefix.toUpperCase() === prefix.toUpperCase())
? prefix
: undefined;
raw = parsed[0].slice(
parsed[0].indexOf(
parsed[1] || hosts.find(host => parsed[0].toUpperCase().includes(host.toUpperCase())) || parsed[2] || parsed[3]
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));
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});
results.actions[actionType].push({ raw, action, slug, prefix, issue });
}
} else if (issue) {
results.refs.push({raw, slug, prefix, 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});
results.mentions.push({ raw: rawMention.trim(), prefix: prefixMention, user });
}
}
}
Expand All @@ -121,13 +118,13 @@ function parse(text, regexp, mentionRegexp, {actions, issuePrefixes, hosts}) {

function typeError(parentOpt, opt) {
return new TypeError(
`The ${[parentOpt, opt].filter(Boolean).join('.')} property must be a String or an array of Strings`
`The ${[parentOpt, opt].filter(Boolean).join(".")} property must be a String or an array of Strings`
);
}

function normalize(options, parentOpt) {
for (const opt of Object.keys(options)) {
if (!parentOpt && opt === 'actions') {
if (!parentOpt && opt === "actions") {
normalize(options[opt], opt);
} else {
if (!options[opt]) {
Expand All @@ -138,7 +135,7 @@ function normalize(options, parentOpt) {
throw typeError(parentOpt, opt);
}

if (options[opt].length !== 0 && !options[opt].every(opt => isString(opt))) {
if (options[opt].length !== 0 && !options[opt].every((opt) => isString(opt))) {
throw typeError(parentOpt, opt);
}

Expand All @@ -147,23 +144,23 @@ function normalize(options, parentOpt) {
}
}

module.exports = (options = 'default', overrides = {}) => {
export default (options = "default", overrides = {}) => {
if (!isString(options) && !isPlainObject(options)) {
throw new TypeError('The options argument must be a String or an Object');
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 (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}'`);
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');
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;
Expand All @@ -172,7 +169,7 @@ module.exports = (options = 'default', overrides = {}) => {
...hostConfig.default,
...options,
...overrides,
actions: {...hostConfig.default.actions, ...options.actions, ...overrides.actions},
actions: { ...hostConfig.default.actions, ...options.actions, ...overrides.actions },
};

normalize(mergedOptions);
Expand All @@ -183,16 +180,16 @@ module.exports = (options = 'default', overrides = {}) => {
const regexp = buildRegexp(mergedOptions);
const mentionRegexp = buildMentionRegexp(mergedOptions);

return text => {
return (text) => {
if (!isString(text)) {
throw new TypeError('The issue text must be a String');
throw new TypeError("The issue text must be a String");
}

const results = parse(text, regexp, mentionRegexp, mergedOptions);

Reflect.defineProperty(results, 'allRefs', {
Reflect.defineProperty(results, "allRefs", {
get() {
return uniqBy(this.refs.concat(...Object.keys(this.actions).map(key => this.actions[key])), 'raw');
return uniqBy(this.refs.concat(...Object.keys(this.actions).map((key) => this.actions[key])), "raw");
},
});
return results;
Expand Down
Loading