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