1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License. See License.txt in the project root for license information.
4
+ *--------------------------------------------------------------------------------------------*/
5
+
6
+ import * as marked from 'marked' ;
7
+ import 'url-search-params-polyfill' ;
8
+ import * as vscode from 'vscode' ;
9
+ import { PullRequestDefaults } from '../github/folderRepositoryManager' ;
10
+ import { GithubItemStateEnum , User } from '../github/interface' ;
11
+ import { IssueModel } from '../github/issueModel' ;
12
+ import { PullRequestModel } from '../github/pullRequestModel' ;
13
+ import { RepositoriesManager } from '../github/repositoriesManager' ;
14
+ import { getIssueNumberLabelFromParsed , ISSUE_OR_URL_EXPRESSION , makeLabel , parseIssueExpressionOutput } from '../github/utils' ;
15
+ import { CODE_PERMALINK , findCodeLinkLocally } from '../issues/issueLinkLookup' ;
16
+ import Logger from './logger' ;
17
+
18
+ function getIconString ( issue : IssueModel ) {
19
+ switch ( issue . state ) {
20
+ case GithubItemStateEnum . Open : {
21
+ return issue instanceof PullRequestModel ? '$(git-pull-request)' : '$(issues)' ;
22
+ }
23
+ case GithubItemStateEnum . Closed : {
24
+ return issue instanceof PullRequestModel ? '$(git-pull-request)' : '$(issue-closed)' ;
25
+ }
26
+ case GithubItemStateEnum . Merged :
27
+ return '$(git-merge)' ;
28
+ }
29
+ }
30
+
31
+ function getIconMarkdown ( issue : IssueModel ) {
32
+ if ( issue instanceof PullRequestModel ) {
33
+ return getIconString ( issue ) ;
34
+ }
35
+ switch ( issue . state ) {
36
+ case GithubItemStateEnum . Open : {
37
+ return `<span style="color:#22863a;">$(issues)</span>` ;
38
+ }
39
+ case GithubItemStateEnum . Closed : {
40
+ return `<span style="color:#cb2431;">$(issue-closed)</span>` ;
41
+ }
42
+ }
43
+ }
44
+
45
+ function repoCommitDate ( user : User , repoNameWithOwner : string ) : string | undefined {
46
+ let date : string | undefined = undefined ;
47
+ user . commitContributions . forEach ( element => {
48
+ if ( repoNameWithOwner . toLowerCase ( ) === element . repoNameWithOwner . toLowerCase ( ) ) {
49
+ date = element . createdAt . toLocaleString ( 'default' , { day : 'numeric' , month : 'short' , year : 'numeric' } ) ;
50
+ }
51
+ } ) ;
52
+ return date ;
53
+ }
54
+
55
+ export function userMarkdown ( origin : PullRequestDefaults , user : User ) : vscode . MarkdownString {
56
+ const markdown : vscode . MarkdownString = new vscode . MarkdownString ( undefined , true ) ;
57
+ markdown . appendMarkdown (
58
+ ` ${ user . name ? `**${ user . name } ** ` : '' } [${ user . login } ](${ user . url } )` ,
59
+ ) ;
60
+ if ( user . bio ) {
61
+ markdown . appendText ( ' \r\n' + user . bio . replace ( / \r \n / g, ' ' ) ) ;
62
+ }
63
+
64
+ const date = repoCommitDate ( user , origin . owner + '/' + origin . repo ) ;
65
+ if ( user . location || date ) {
66
+ markdown . appendMarkdown ( ' \r\n\r\n---' ) ;
67
+ }
68
+ if ( user . location ) {
69
+ markdown . appendMarkdown ( ` \r\n${ vscode . l10n . t ( '{0} {1}' , '$(location)' , user . location ) } ` ) ;
70
+ }
71
+ if ( date ) {
72
+ markdown . appendMarkdown ( ` \r\n${ vscode . l10n . t ( '{0} Committed to this repository on {1}' , '$(git-commit)' , date ) } ` ) ;
73
+ }
74
+ if ( user . company ) {
75
+ markdown . appendMarkdown ( ` \r\n${ vscode . l10n . t ( { message : '{0} Member of {1}' , args : [ '$(jersey)' , user . company ] , comment : [ 'An organization that the user is a member of.' , 'The first placeholder is an icon and shouldn\'t be localized.' , 'The second placeholder is the name of the organization.' ] } ) } ` ) ;
76
+ }
77
+ return markdown ;
78
+ }
79
+
80
+ async function findAndModifyString (
81
+ text : string ,
82
+ find : RegExp ,
83
+ transformer : ( match : RegExpMatchArray ) => Promise < string | undefined > ,
84
+ ) : Promise < string > {
85
+ let searchResult = text . search ( find ) ;
86
+ let position = 0 ;
87
+ while ( searchResult >= 0 && searchResult < text . length ) {
88
+ let newBodyFirstPart : string | undefined ;
89
+ if ( searchResult === 0 || text . charAt ( searchResult - 1 ) !== '&' ) {
90
+ const match = text . substring ( searchResult ) . match ( find ) ! ;
91
+ if ( match ) {
92
+ const transformed = await transformer ( match ) ;
93
+ if ( transformed ) {
94
+ newBodyFirstPart = text . slice ( 0 , searchResult ) + transformed ;
95
+ text = newBodyFirstPart + text . slice ( searchResult + match [ 0 ] . length ) ;
96
+ }
97
+ }
98
+ }
99
+ position = newBodyFirstPart ? newBodyFirstPart . length : searchResult + 1 ;
100
+ const newSearchResult = text . substring ( position ) . search ( find ) ;
101
+ searchResult = newSearchResult > 0 ? position + newSearchResult : newSearchResult ;
102
+ }
103
+ return text ;
104
+ }
105
+
106
+ function findLinksInIssue ( body : string , issue : IssueModel ) : Promise < string > {
107
+ return findAndModifyString ( body , ISSUE_OR_URL_EXPRESSION , async ( match : RegExpMatchArray ) => {
108
+ const tryParse = parseIssueExpressionOutput ( match ) ;
109
+ if ( tryParse ) {
110
+ const issueNumberLabel = getIssueNumberLabelFromParsed ( tryParse ) ; // get label before setting owner and name.
111
+ if ( ! tryParse . owner || ! tryParse . name ) {
112
+ tryParse . owner = issue . remote . owner ;
113
+ tryParse . name = issue . remote . repositoryName ;
114
+ }
115
+ return `[${ issueNumberLabel } ](https://github.com/${ tryParse . owner } /${ tryParse . name } /issues/${ tryParse . issueNumber } )` ;
116
+ }
117
+ return undefined ;
118
+ } ) ;
119
+ }
120
+
121
+ async function findCodeLinksInIssue ( body : string , repositoriesManager : RepositoriesManager ) {
122
+ return findAndModifyString ( body , CODE_PERMALINK , async ( match : RegExpMatchArray ) => {
123
+ const codeLink = await findCodeLinkLocally ( match , repositoriesManager ) ;
124
+ if ( codeLink ) {
125
+ Logger . trace ( 'finding code links in issue' , 'Issues' ) ;
126
+ const textDocument = await vscode . workspace . openTextDocument ( codeLink ?. file ) ;
127
+ const endingTextDocumentLine = textDocument . lineAt (
128
+ codeLink . end < textDocument . lineCount ? codeLink . end : textDocument . lineCount - 1 ,
129
+ ) ;
130
+ const query = [
131
+ codeLink . file ,
132
+ {
133
+ selection : {
134
+ start : {
135
+ line : codeLink . start ,
136
+ character : 0 ,
137
+ } ,
138
+ end : {
139
+ line : codeLink . end ,
140
+ character : endingTextDocumentLine . text . length ,
141
+ } ,
142
+ } ,
143
+ } ,
144
+ ] ;
145
+ const openCommand = vscode . Uri . parse ( `command:vscode.open?${ encodeURIComponent ( JSON . stringify ( query ) ) } ` ) ;
146
+ return `[${ match [ 0 ] } ](${ openCommand } "Open ${ codeLink . file . fsPath } ")` ;
147
+ }
148
+ return undefined ;
149
+ } ) ;
150
+ }
151
+
152
+ export const ISSUE_BODY_LENGTH : number = 200 ;
153
+ export async function issueMarkdown (
154
+ issue : IssueModel ,
155
+ context : vscode . ExtensionContext ,
156
+ repositoriesManager : RepositoriesManager ,
157
+ commentNumber ?: number ,
158
+ ) : Promise < vscode . MarkdownString > {
159
+ const markdown : vscode . MarkdownString = new vscode . MarkdownString ( undefined , true ) ;
160
+ markdown . supportHtml = true ;
161
+ const date = new Date ( issue . createdAt ) ;
162
+ const ownerName = `${ issue . remote . owner } /${ issue . remote . repositoryName } ` ;
163
+ markdown . appendMarkdown (
164
+ `[${ ownerName } ](https://github.com/${ ownerName } ) on ${ date . toLocaleString ( 'default' , {
165
+ day : 'numeric' ,
166
+ month : 'short' ,
167
+ year : 'numeric' ,
168
+ } ) } \n`,
169
+ ) ;
170
+ const title = marked
171
+ . parse ( issue . title , {
172
+ renderer : new PlainTextRenderer ( ) ,
173
+ } )
174
+ . trim ( ) ;
175
+ markdown . appendMarkdown (
176
+ `${ getIconMarkdown ( issue ) } **${ title } ** [#${ issue . number } ](${ issue . html_url } ) \n` ,
177
+ ) ;
178
+ let body = marked . parse ( issue . body , {
179
+ renderer : new PlainTextRenderer ( ) ,
180
+ } ) ;
181
+ markdown . appendMarkdown ( ' \n' ) ;
182
+ body = body . length > ISSUE_BODY_LENGTH ? body . substr ( 0 , ISSUE_BODY_LENGTH ) + '...' : body ;
183
+ body = await findLinksInIssue ( body , issue ) ;
184
+ body = await findCodeLinksInIssue ( body , repositoriesManager ) ;
185
+
186
+ markdown . appendMarkdown ( body + ' \n' ) ;
187
+ markdown . appendMarkdown ( ' \n' ) ;
188
+
189
+ if ( issue . item . labels . length > 0 ) {
190
+ issue . item . labels . forEach ( label => {
191
+ markdown . appendMarkdown (
192
+ `[${ makeLabel ( label ) } ](https://github.com/${ ownerName } /labels/${ encodeURIComponent (
193
+ label . name ,
194
+ ) } ) `,
195
+ ) ;
196
+ } ) ;
197
+ }
198
+
199
+ if ( issue . item . comments && commentNumber ) {
200
+ for ( const comment of issue . item . comments ) {
201
+ if ( comment . databaseId === commentNumber ) {
202
+ markdown . appendMarkdown ( ' \r\n\r\n---\r\n' ) ;
203
+ markdown . appendMarkdown ( ' \n' ) ;
204
+ markdown . appendMarkdown (
205
+ ` **${ comment . author . login } ** commented` ,
206
+ ) ;
207
+ markdown . appendMarkdown ( ' \n' ) ;
208
+ let commentText = marked . parse (
209
+ comment . body . length > ISSUE_BODY_LENGTH
210
+ ? comment . body . substr ( 0 , ISSUE_BODY_LENGTH ) + '...'
211
+ : comment . body ,
212
+ { renderer : new PlainTextRenderer ( ) } ,
213
+ ) ;
214
+ commentText = await findLinksInIssue ( commentText , issue ) ;
215
+ markdown . appendMarkdown ( commentText ) ;
216
+ }
217
+ }
218
+ }
219
+ return markdown ;
220
+ }
221
+
222
+ export class PlainTextRenderer extends marked . Renderer {
223
+ override code ( code : string , _infostring : string | undefined ) : string {
224
+ return code ;
225
+ }
226
+ override blockquote ( quote : string ) : string {
227
+ return quote ;
228
+ }
229
+ override html ( _html : string ) : string {
230
+ return '' ;
231
+ }
232
+ override heading ( text : string , _level : 1 | 2 | 3 | 4 | 5 | 6 , _raw : string , _slugger : marked . Slugger ) : string {
233
+ return text + ' ' ;
234
+ }
235
+ override hr ( ) : string {
236
+ return '' ;
237
+ }
238
+ override list ( body : string , _ordered : boolean , _start : number ) : string {
239
+ return body ;
240
+ }
241
+ override listitem ( text : string ) : string {
242
+ return ' ' + text ;
243
+ }
244
+ override checkbox ( _checked : boolean ) : string {
245
+ return '' ;
246
+ }
247
+ override paragraph ( text : string ) : string {
248
+ return text . replace ( / \< / g, '\\\<' ) . replace ( / \> / g, '\\\>' ) + ' ' ;
249
+ }
250
+ override table ( header : string , body : string ) : string {
251
+ return header + ' ' + body ;
252
+ }
253
+ override tablerow ( content : string ) : string {
254
+ return content ;
255
+ }
256
+ override tablecell (
257
+ content : string ,
258
+ _flags : {
259
+ header : boolean ;
260
+ align : 'center' | 'left' | 'right' | null ;
261
+ } ,
262
+ ) : string {
263
+ return content ;
264
+ }
265
+ override strong ( text : string ) : string {
266
+ return text ;
267
+ }
268
+ override em ( text : string ) : string {
269
+ return text ;
270
+ }
271
+ override codespan ( code : string ) : string {
272
+ return `\\\`${ code } \\\`` ;
273
+ }
274
+ override br ( ) : string {
275
+ return ' ' ;
276
+ }
277
+ override del ( text : string ) : string {
278
+ return text ;
279
+ }
280
+ override image ( _href : string , _title : string , _text : string ) : string {
281
+ return '' ;
282
+ }
283
+ override text ( text : string ) : string {
284
+ return text ;
285
+ }
286
+ override link ( href : string , title : string , text : string ) : string {
287
+ return text + ' ' ;
288
+ }
289
+ }
0 commit comments