Skip to content

Commit c792738

Browse files
Merge pull request #9 from maxim-lobanov/mlobanov/parse-short-issue-links
Implement parsing of short issue links
2 parents 27ef05b + 2b71df2 commit c792738

10 files changed

+228
-87
lines changed

dist/index.js

+48-29
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,11 @@ class IssueContentParser {
173173
.map(x => (0, utils_1.parseIssueUrl)(x))
174174
.filter((x) => x !== null);
175175
}
176-
extractIssueDependencies(issue) {
176+
extractIssueDependencies(issue, repoRef) {
177177
const contentLines = issue.body?.split("\n") ?? [];
178178
return contentLines
179179
.filter(x => this.isDependencyLine(x))
180-
.map(x => (0, utils_1.parseIssuesUrls)(x))
180+
.map(x => (0, utils_1.parseIssuesUrls)(x, repoRef))
181181
.flat()
182182
.filter((x) => x !== null);
183183
}
@@ -291,7 +291,7 @@ const run = async () => {
291291
for (const issueRef of rootIssueTasklist) {
292292
const issue = await githubApiClient.getIssue(issueRef);
293293
const issueDetails = mermaid_node_1.MermaidNode.createFromGitHubIssue(issue);
294-
const issueDependencies = issueContentParser.extractIssueDependencies(issue);
294+
const issueDependencies = issueContentParser.extractIssueDependencies(issue, issueRef);
295295
graphBuilder.addIssue(issueRef, issueDetails);
296296
issueDependencies.forEach(x => graphBuilder.addDependency(x, issueRef));
297297
}
@@ -330,35 +330,24 @@ run();
330330
/***/ }),
331331

332332
/***/ 235:
333-
/***/ ((__unused_webpack_module, exports) => {
333+
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
334334

335335
"use strict";
336336

337337
Object.defineProperty(exports, "__esModule", ({ value: true }));
338338
exports.MermaidNode = void 0;
339+
const utils_1 = __nccwpck_require__(918);
339340
class MermaidNode {
340341
constructor(nodeId, title, status, url) {
341342
this.nodeId = nodeId;
342343
this.title = title;
343344
this.status = status;
344345
this.url = url;
345346
}
346-
getWrappedTitle() {
347-
const maxWidth = 40;
348-
const words = this.title.split(/\s+/);
349-
let result = words[0];
350-
let lastLength = result.length;
351-
for (let wordIndex = 1; wordIndex < words.length; wordIndex++) {
352-
if (lastLength + words[wordIndex].length >= maxWidth) {
353-
result += "\n";
354-
lastLength = 0;
355-
}
356-
else {
357-
result += " ";
358-
}
359-
result += words[wordIndex];
360-
lastLength += words[wordIndex].length;
361-
}
347+
getFormattedTitle() {
348+
let result = this.title;
349+
result = result.replaceAll('"', "'");
350+
result = (0, utils_1.wrapString)(result, 40);
362351
return result;
363352
}
364353
static createFromGitHubIssue(issue) {
@@ -447,7 +436,7 @@ ${renderedGraphIssues}
447436
`;
448437
}
449438
renderIssue(issue) {
450-
const title = issue.getWrappedTitle();
439+
const title = issue.getFormattedTitle();
451440
const linkedTitle = issue.url
452441
? `<a href='${issue.url}' style='text-decoration:none;color: inherit;'>${title}</a>`
453442
: title;
@@ -478,9 +467,10 @@ exports.MermaidRender = MermaidRender;
478467
"use strict";
479468

480469
Object.defineProperty(exports, "__esModule", ({ value: true }));
481-
exports.parseIssuesUrls = exports.parseIssueUrl = void 0;
470+
exports.wrapString = exports.parseIssuesUrls = exports.parseIssueNumber = exports.parseIssueUrl = void 0;
482471
const issueUrlRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)$/i;
483-
const issueUrlsRegex = /github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/gi;
472+
const issueNumberRegex = /^#(\d+)$/;
473+
const issueUrlsRegex = /https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)|#\d+/gi;
484474
const parseIssueUrl = (str) => {
485475
const found = str.trim().match(issueUrlRegex);
486476
if (!found) {
@@ -493,18 +483,47 @@ const parseIssueUrl = (str) => {
493483
};
494484
};
495485
exports.parseIssueUrl = parseIssueUrl;
496-
const parseIssuesUrls = (str) => {
486+
const parseIssueNumber = (str, repoRef) => {
487+
const found = str.trim().match(issueNumberRegex);
488+
if (!found) {
489+
return null;
490+
}
491+
return {
492+
repoOwner: repoRef.repoOwner,
493+
repoName: repoRef.repoName,
494+
issueNumber: parseInt(found[1]),
495+
};
496+
};
497+
exports.parseIssueNumber = parseIssueNumber;
498+
const parseIssuesUrls = (str, repoRef) => {
497499
const result = [];
498500
for (const match of str.matchAll(issueUrlsRegex)) {
499-
result.push({
500-
repoOwner: match[1],
501-
repoName: match[2],
502-
issueNumber: parseInt(match[3]),
503-
});
501+
const parsedIssue = (0, exports.parseIssueUrl)(match[0]) || (0, exports.parseIssueNumber)(match[0], repoRef);
502+
if (parsedIssue) {
503+
result.push(parsedIssue);
504+
}
504505
}
505506
return result;
506507
};
507508
exports.parseIssuesUrls = parseIssuesUrls;
509+
const wrapString = (str, maxWidth) => {
510+
const words = str.split(/\s+/);
511+
let result = words[0];
512+
let lastLength = result.length;
513+
for (let wordIndex = 1; wordIndex < words.length; wordIndex++) {
514+
if (lastLength + words[wordIndex].length >= maxWidth) {
515+
result += "\n";
516+
lastLength = 0;
517+
}
518+
else {
519+
result += " ";
520+
}
521+
result += words[wordIndex];
522+
lastLength += words[wordIndex].length;
523+
}
524+
return result;
525+
};
526+
exports.wrapString = wrapString;
508527

509528

510529
/***/ }),

src/issue-content-parser.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GitHubIssue, GitHubIssueReference } from "./models";
1+
import { GitHubIssue, GitHubIssueReference, GitHubRepoReference } from "./models";
22
import { parseIssuesUrls, parseIssueUrl } from "./utils";
33

44
export class IssueContentParser {
@@ -12,12 +12,12 @@ export class IssueContentParser {
1212
.filter((x): x is GitHubIssueReference => x !== null);
1313
}
1414

15-
public extractIssueDependencies(issue: GitHubIssue): GitHubIssueReference[] {
15+
public extractIssueDependencies(issue: GitHubIssue, repoRef: GitHubRepoReference): GitHubIssueReference[] {
1616
const contentLines = issue.body?.split("\n") ?? [];
1717

1818
return contentLines
1919
.filter(x => this.isDependencyLine(x))
20-
.map(x => parseIssuesUrls(x))
20+
.map(x => parseIssuesUrls(x, repoRef))
2121
.flat()
2222
.filter((x): x is GitHubIssueReference => x !== null);
2323
}

src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const run = async (): Promise<void> => {
3131
for (const issueRef of rootIssueTasklist) {
3232
const issue = await githubApiClient.getIssue(issueRef);
3333
const issueDetails = MermaidNode.createFromGitHubIssue(issue);
34-
const issueDependencies = issueContentParser.extractIssueDependencies(issue);
34+
const issueDependencies = issueContentParser.extractIssueDependencies(issue, issueRef);
3535
graphBuilder.addIssue(issueRef, issueDetails);
3636
issueDependencies.forEach(x => graphBuilder.addDependency(x, issueRef));
3737
}

src/mermaid-node.ts

+6-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GitHubIssue } from "./models";
2+
import { wrapString } from "./utils";
23

34
export type MermaidNodeStatus = "default" | "notstarted" | "started" | "completed";
45

@@ -10,23 +11,11 @@ export class MermaidNode {
1011
public readonly url?: string
1112
) {}
1213

13-
public getWrappedTitle(): string {
14-
const maxWidth = 40;
15-
const words = this.title.split(/\s+/);
16-
17-
let result = words[0];
18-
let lastLength = result.length;
19-
for (let wordIndex = 1; wordIndex < words.length; wordIndex++) {
20-
if (lastLength + words[wordIndex].length >= maxWidth) {
21-
result += "\n";
22-
lastLength = 0;
23-
} else {
24-
result += " ";
25-
}
26-
27-
result += words[wordIndex];
28-
lastLength += words[wordIndex].length;
29-
}
14+
public getFormattedTitle(): string {
15+
let result = this.title;
16+
17+
result = result.replaceAll('"', "'");
18+
result = wrapString(result, 40);
3019

3120
return result;
3221
}

src/mermaid-render.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ ${renderedGraphIssues}
6161
}
6262

6363
private renderIssue(issue: MermaidNode): string {
64-
const title = issue.getWrappedTitle();
64+
const title = issue.getFormattedTitle();
6565
const linkedTitle = issue.url
6666
? `<a href='${issue.url}' style='text-decoration:none;color: inherit;'>${title}</a>`
6767
: title;

src/models.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
export type GitHubIssueReference = {
1+
export type GitHubRepoReference = {
22
repoOwner: string;
33
repoName: string;
4+
};
5+
6+
export type GitHubIssueReference = GitHubRepoReference & {
47
issueNumber: number;
58
};
69

src/utils.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { GitHubIssueReference } from "./models";
1+
import { GitHubIssueReference, GitHubRepoReference } from "./models";
22

33
// Analogue of TypeScript "Partial" type but for null values
44
export type NullablePartial<T> = { [P in keyof T]: T[P] | null | undefined };
55

66
const issueUrlRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)$/i;
7-
const issueUrlsRegex = /github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/gi;
7+
const issueNumberRegex = /^#(\d+)$/;
8+
const issueUrlsRegex = /https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)|#\d+/gi;
89

910
export const parseIssueUrl = (str: string): GitHubIssueReference | null => {
1011
const found = str.trim().match(issueUrlRegex);
@@ -19,15 +20,47 @@ export const parseIssueUrl = (str: string): GitHubIssueReference | null => {
1920
};
2021
};
2122

22-
export const parseIssuesUrls = (str: string): GitHubIssueReference[] => {
23+
export const parseIssueNumber = (str: string, repoRef: GitHubRepoReference): GitHubIssueReference | null => {
24+
const found = str.trim().match(issueNumberRegex);
25+
if (!found) {
26+
return null;
27+
}
28+
29+
return {
30+
repoOwner: repoRef.repoOwner,
31+
repoName: repoRef.repoName,
32+
issueNumber: parseInt(found[1]),
33+
};
34+
};
35+
36+
export const parseIssuesUrls = (str: string, repoRef: GitHubRepoReference): GitHubIssueReference[] => {
2337
const result: GitHubIssueReference[] = [];
2438

2539
for (const match of str.matchAll(issueUrlsRegex)) {
26-
result.push({
27-
repoOwner: match[1],
28-
repoName: match[2],
29-
issueNumber: parseInt(match[3]),
30-
});
40+
const parsedIssue = parseIssueUrl(match[0]) || parseIssueNumber(match[0], repoRef);
41+
if (parsedIssue) {
42+
result.push(parsedIssue);
43+
}
44+
}
45+
46+
return result;
47+
};
48+
49+
export const wrapString = (str: string, maxWidth: number): string => {
50+
const words = str.split(/\s+/);
51+
52+
let result = words[0];
53+
let lastLength = result.length;
54+
for (let wordIndex = 1; wordIndex < words.length; wordIndex++) {
55+
if (lastLength + words[wordIndex].length >= maxWidth) {
56+
result += "\n";
57+
lastLength = 0;
58+
} else {
59+
result += " ";
60+
}
61+
62+
result += words[wordIndex];
63+
lastLength += words[wordIndex].length;
3164
}
3265

3366
return result;

tests/issue-content-parser.test.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IssueContentParser } from "../src/issue-content-parser";
2-
import { GitHubIssue } from "../src/models";
2+
import { GitHubIssue, GitHubRepoReference } from "../src/models";
33

44
describe("IssueContentParser", () => {
55
const issueContentParser = new IssueContentParser();
@@ -83,9 +83,11 @@ Test content 2
8383
});
8484

8585
describe("extractIssueDependencies", () => {
86+
const repoRef: GitHubRepoReference = { repoOwner: "testOwner", repoName: "testRepo" };
87+
8688
it("empty body", () => {
8789
const issue = { body: undefined } as GitHubIssue;
88-
const actual = issueContentParser.extractIssueDependencies(issue);
90+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
8991
expect(actual).toEqual([]);
9092
});
9193

@@ -101,15 +103,15 @@ https://github.com/actions/setup-node/issues/4
101103
Test content 3
102104
`,
103105
} as GitHubIssue;
104-
const actual = issueContentParser.extractIssueDependencies(issue);
106+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
105107
expect(actual).toEqual([]);
106108
});
107109

108110
it("single dependency line with single issue", () => {
109111
const issue = {
110112
body: "## Hello\nDepends on https://github.com/actions/setup-node/issues/5663\nTest",
111113
} as GitHubIssue;
112-
const actual = issueContentParser.extractIssueDependencies(issue);
114+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
113115
expect(actual).toEqual([{ repoOwner: "actions", repoName: "setup-node", issueNumber: 5663 }]);
114116
});
115117

@@ -123,7 +125,7 @@ Depends on https://github.com/actions/setup-node/issues/105, https://github.com/
123125
Test content
124126
`,
125127
} as GitHubIssue;
126-
const actual = issueContentParser.extractIssueDependencies(issue);
128+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
127129
expect(actual).toEqual([
128130
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 105 },
129131
{ repoOwner: "actions", repoName: "setup-python", issueNumber: 115 },
@@ -143,7 +145,7 @@ Depends on https://github.com/actions/setup-ruby/issues/105 & https://github.com
143145
Test content
144146
`,
145147
} as GitHubIssue;
146-
const actual = issueContentParser.extractIssueDependencies(issue);
148+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
147149
expect(actual).toEqual([
148150
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 101 },
149151
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 102 },
@@ -166,11 +168,36 @@ Dependencies: https://github.com/actions/setup-node/issues/103
166168
Test content
167169
`,
168170
} as GitHubIssue;
169-
const actual = issueContentParser.extractIssueDependencies(issue);
171+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
172+
expect(actual).toEqual([
173+
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 101 },
174+
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 102 },
175+
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 103 },
176+
]);
177+
});
178+
179+
it("diffent types of issues referencing", () => {
180+
const issue = {
181+
body: `
182+
Hello
183+
184+
Depends on https://github.com/actions/setup-node/issues/101
185+
depends on: https://github.com/actions/setup-node/issues/102
186+
Dependencies: https://github.com/actions/setup-node/issues/103
187+
Depends on: #123, #456, https://github.com/actions/setup-node/issues/105, #701
188+
189+
Test content
190+
`,
191+
} as GitHubIssue;
192+
const actual = issueContentParser.extractIssueDependencies(issue, repoRef);
170193
expect(actual).toEqual([
171194
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 101 },
172195
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 102 },
173196
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 103 },
197+
{ repoOwner: "testOwner", repoName: "testRepo", issueNumber: 123 },
198+
{ repoOwner: "testOwner", repoName: "testRepo", issueNumber: 456 },
199+
{ repoOwner: "actions", repoName: "setup-node", issueNumber: 105 },
200+
{ repoOwner: "testOwner", repoName: "testRepo", issueNumber: 701 },
174201
]);
175202
});
176203
});

0 commit comments

Comments
 (0)