1+ import { CommentType , CommentContext , BaseTextareaHandler , TextareaInfo } from '../datamodel/textarea-handler' ;
2+
3+ export interface GitHubContext extends CommentContext {
4+ domain : string ;
5+ slug : string ; // owner/repo
6+ number ?: number ; // issue/PR number
7+ commentId ?: string ; // for editing existing comments
8+ }
9+
10+ export class GitHubHandler extends BaseTextareaHandler < GitHubContext > {
11+ constructor ( ) {
12+ super ( 'github.com' ) ;
13+ }
14+
15+ forCommentTypes ( ) : CommentType [ ] {
16+ return [
17+ 'GH_ISSUE_NEW' ,
18+ 'GH_PR_NEW' ,
19+ 'GH_ISSUE_ADD_COMMENT' ,
20+ 'GH_ISSUE_EDIT_COMMENT' ,
21+ 'GH_PR_ADD_COMMENT' ,
22+ 'GH_PR_EDIT_COMMENT' ,
23+ 'GH_PR_CODE_COMMENT'
24+ ] ;
25+ }
26+
27+ identify ( ) : TextareaInfo < GitHubContext > [ ] {
28+ const textareas = document . querySelectorAll < HTMLTextAreaElement > ( 'textarea' ) ;
29+ const results : TextareaInfo < GitHubContext > [ ] = [ ] ;
30+
31+ for ( const textarea of textareas ) {
32+ const type = this . determineType ( textarea ) ;
33+ const context = this . extractContext ( textarea ) ;
34+
35+ if ( type && context ) {
36+ results . push ( { element : textarea , type, context } ) ;
37+ }
38+ }
39+
40+ return results ;
41+ }
42+
43+ extractContext ( textarea : HTMLTextAreaElement ) : GitHubContext | null {
44+ const url = window . location . href ;
45+ const pathname = window . location . pathname ;
46+
47+ // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
48+ const match = pathname . match ( / ^ \/ ( [ ^ \/ ] + ) \/ ( [ ^ \/ ] + ) (?: \/ ( i s s u e s | p u l l ) \/ ( \d + ) ) ? / ) ;
49+ if ( ! match ) return null ;
50+
51+ const [ , owner , repo , type , numberStr ] = match ;
52+ const slug = `${ owner } /${ repo } ` ;
53+ const number = numberStr ? parseInt ( numberStr , 10 ) : undefined ;
54+
55+ // Generate unique key based on context
56+ let unique_key = `github:${ slug } ` ;
57+ if ( number ) {
58+ unique_key += `:${ type } :${ number } ` ;
59+ } else {
60+ unique_key += ':new' ;
61+ }
62+
63+ // Check if editing existing comment
64+ const commentId = this . getCommentId ( textarea ) ;
65+ if ( commentId ) {
66+ unique_key += `:edit:${ commentId } ` ;
67+ }
68+
69+ return {
70+ unique_key,
71+ domain : window . location . hostname ,
72+ slug,
73+ number,
74+ commentId
75+ } ;
76+ }
77+
78+ determineType ( textarea : HTMLTextAreaElement ) : CommentType | null {
79+ const pathname = window . location . pathname ;
80+
81+ // New issue
82+ if ( pathname . includes ( '/issues/new' ) ) {
83+ return 'GH_ISSUE_NEW' ;
84+ }
85+
86+ // New PR
87+ if ( pathname . includes ( '/compare/' ) || pathname . endsWith ( '/compare' ) ) {
88+ return 'GH_PR_NEW' ;
89+ }
90+
91+ // Check if we're on an issue or PR page
92+ const match = pathname . match ( / \/ ( i s s u e s | p u l l ) \/ ( \d + ) / ) ;
93+ if ( ! match ) return null ;
94+
95+ const [ , type ] = match ;
96+ const isEditingComment = this . getCommentId ( textarea ) !== null ;
97+
98+ if ( type === 'issues' ) {
99+ return isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT' ;
100+ } else {
101+ // Check if it's a code comment (in Files Changed tab)
102+ const isCodeComment = textarea . closest ( '.js-inline-comment-form' ) !== null ||
103+ textarea . closest ( '[data-path]' ) !== null ;
104+
105+ if ( isCodeComment ) {
106+ return 'GH_PR_CODE_COMMENT' ;
107+ }
108+
109+ return isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT' ;
110+ }
111+ }
112+
113+ generateDisplayTitle ( context : GitHubContext ) : string {
114+ const { slug, number, commentId } = context ;
115+
116+ if ( commentId ) {
117+ return `Edit comment in ${ slug } ${ number ? ` #${ number } ` : '' } ` ;
118+ }
119+
120+ if ( number ) {
121+ return `Comment on ${ slug } #${ number } ` ;
122+ }
123+
124+ return `New ${ window . location . pathname . includes ( '/issues/' ) ? 'issue' : 'PR' } in ${ slug } ` ;
125+ }
126+
127+ generateIcon ( type : CommentType ) : string {
128+ switch ( type ) {
129+ case 'GH_ISSUE_NEW' :
130+ case 'GH_ISSUE_ADD_COMMENT' :
131+ case 'GH_ISSUE_EDIT_COMMENT' :
132+ return '🐛' ; // Issue icon
133+ case 'GH_PR_NEW' :
134+ case 'GH_PR_ADD_COMMENT' :
135+ case 'GH_PR_EDIT_COMMENT' :
136+ return '🔄' ; // PR icon
137+ case 'GH_PR_CODE_COMMENT' :
138+ return '💬' ; // Code comment icon
139+ default :
140+ return '📝' ; // Generic comment icon
141+ }
142+ }
143+
144+ buildUrl ( context : GitHubContext , withDraft ?: boolean ) : string {
145+ const baseUrl = `https://${ context . domain } /${ context . slug } ` ;
146+
147+ if ( context . number ) {
148+ const type = window . location . pathname . includes ( '/issues/' ) ? 'issues' : 'pull' ;
149+ return `${ baseUrl } /${ type } /${ context . number } ${ context . commentId ? `#issuecomment-${ context . commentId } ` : '' } ` ;
150+ }
151+
152+ return baseUrl ;
153+ }
154+
155+ private getCommentId ( textarea : HTMLTextAreaElement ) : string | null {
156+ // Look for edit comment form indicators
157+ const commentForm = textarea . closest ( '[data-comment-id]' ) ;
158+ if ( commentForm ) {
159+ return commentForm . getAttribute ( 'data-comment-id' ) ;
160+ }
161+
162+ const editForm = textarea . closest ( '.js-comment-edit-form' ) ;
163+ if ( editForm ) {
164+ const commentContainer = editForm . closest ( '.js-comment-container' ) ;
165+ if ( commentContainer ) {
166+ const id = commentContainer . getAttribute ( 'data-gid' ) ||
167+ commentContainer . getAttribute ( 'id' ) ;
168+ return id ? id . replace ( 'issuecomment-' , '' ) : null ;
169+ }
170+ }
171+
172+ return null ;
173+ }
174+ }
0 commit comments