@@ -1014,4 +1014,165 @@ export function truncate(value: string, maxLength: number, suffix = '...'): stri
10141014 return value ;
10151015 }
10161016 return `${ value . substr ( 0 , maxLength ) } ${ suffix } ` ;
1017+ }
1018+
1019+ /**
1020+ * Metadata extracted from code reference link data attributes.
1021+ * This interface defines the contract between the extension (which creates the attributes)
1022+ * and the webview (which reads them).
1023+ */
1024+ export interface CodeReferenceLinkMetadata {
1025+ localFile : string ;
1026+ startLine : number ;
1027+ endLine : number ;
1028+ linkType : 'blob' | 'diff' ;
1029+ href : string ;
1030+ }
1031+
1032+ /**
1033+ * Extracts code reference link metadata from an anchor element's data attributes.
1034+ * Returns null if any required attributes are missing.
1035+ */
1036+ export function extractCodeReferenceLinkMetadata ( anchor : Element ) : CodeReferenceLinkMetadata | null {
1037+ const localFile = anchor . getAttribute ( 'data-local-file' ) ;
1038+ const startLine = anchor . getAttribute ( 'data-start-line' ) ;
1039+ const endLine = anchor . getAttribute ( 'data-end-line' ) ;
1040+ const linkType = anchor . getAttribute ( 'data-link-type' ) ;
1041+ const href = anchor . getAttribute ( 'href' ) ;
1042+
1043+ if ( ! localFile || ! startLine || ! endLine || ! linkType || ! href ) {
1044+ return null ;
1045+ }
1046+
1047+ return {
1048+ localFile,
1049+ startLine : parseInt ( startLine ) ,
1050+ endLine : parseInt ( endLine ) ,
1051+ linkType : linkType as 'blob' | 'diff' ,
1052+ href
1053+ } ;
1054+ }
1055+
1056+ /**
1057+ * Process GitHub blob permalinks in HTML and add data attributes for local file handling.
1058+ * Finds blob permalinks (e.g., /blob/[sha]/file.ts#L10), checks if files exist locally,
1059+ * and adds data attributes to enable clicking to open local files.
1060+ *
1061+ * @param bodyHTML - The HTML content to process
1062+ * @param repoOwner - GitHub repository owner
1063+ * @param repoName - GitHub repository name
1064+ * @param authority - Git protocol URL authority (e.g., 'github.com')
1065+ * @param fileExistsCheck - Async function that checks if a file exists locally given its relative path
1066+ * @returns Promise resolving to processed HTML
1067+ */
1068+ export async function processPermalinks (
1069+ bodyHTML : string ,
1070+ repoOwner : string ,
1071+ repoName : string ,
1072+ authority : string ,
1073+ fileExistsCheck : ( filePath : string ) => Promise < boolean >
1074+ ) : Promise < string > {
1075+ try {
1076+ const escapedRepoName = escapeRegExp ( repoName ) ;
1077+ const escapedRepoOwner = escapeRegExp ( repoOwner ) ;
1078+ const escapedAuthority = escapeRegExp ( authority ) ;
1079+
1080+ // Process blob permalinks (exclude already processed links)
1081+ const blobPattern = new RegExp (
1082+ `<a\\s+(?![^>]*data-permalink-processed)([^>]*?href="https?:\/\/${ escapedAuthority } \/${ escapedRepoOwner } \/${ escapedRepoName } \/blob\/[0-9a-f]{40}\/(?<filePath>[^"#]+)#L(?<startLine>\\d+)(?:-L(?<endLine>\\d+))?"[^>]*?)>(?<linkText>[^<]*?)<\/a>` ,
1083+ 'g'
1084+ ) ;
1085+
1086+ return await stringReplaceAsync ( bodyHTML , blobPattern , async (
1087+ fullMatch : string ,
1088+ attributes : string ,
1089+ filePath : string ,
1090+ startLine : string ,
1091+ endLine : string | undefined ,
1092+ linkText : string
1093+ ) => {
1094+ try {
1095+ // Extract the original URL from attributes
1096+ const hrefMatch = attributes . match ( / h r e f = " ( [ ^ " ] + ) " / ) ;
1097+ const originalUrl = hrefMatch ? hrefMatch [ 1 ] : '' ;
1098+
1099+ // Check if file exists locally
1100+ const exists = await fileExistsCheck ( filePath ) ;
1101+ if ( exists ) {
1102+ // File exists - add data attributes for local handling and "(view on GitHub)" suffix
1103+ const endLineValue = endLine || startLine ;
1104+ return `<a data-permalink-processed="true" ${ attributes } data-local-file="${ filePath } " data-start-line="${ startLine } " data-end-line="${ endLineValue } " data-link-type="blob">${ linkText } </a> (<a data-permalink-processed="true" href="${ originalUrl } ">view on GitHub</a>)` ;
1105+ }
1106+ } catch ( error ) {
1107+ // File doesn't exist or check failed - keep original link
1108+ }
1109+ return fullMatch ;
1110+ } ) ;
1111+ } catch ( error ) {
1112+ // Return original HTML if processing fails
1113+ return bodyHTML ;
1114+ }
1115+ }
1116+
1117+ /**
1118+ * Process GitHub diff permalinks in HTML and add data attributes for local file handling.
1119+ * Finds diff permalinks (e.g., /pull/123/files#diff-[hash]R10), maps hashes to filenames,
1120+ * and adds data attributes to enable clicking to open diff views.
1121+ *
1122+ * @param bodyHTML - The HTML content to process
1123+ * @param repoOwner - GitHub repository owner
1124+ * @param repoName - GitHub repository name
1125+ * @param authority - Git protocol URL authority (e.g., 'github.com')
1126+ * @param hashMap - Map of diff hashes to file paths
1127+ * @param prNumber - Pull request number
1128+ * @returns Promise resolving to processed HTML
1129+ */
1130+ export async function processDiffLinks (
1131+ bodyHTML : string ,
1132+ repoOwner : string ,
1133+ repoName : string ,
1134+ authority : string ,
1135+ hashMap : Record < string , string > ,
1136+ prNumber : number
1137+ ) : Promise < string > {
1138+ try {
1139+ const escapedRepoName = escapeRegExp ( repoName ) ;
1140+ const escapedRepoOwner = escapeRegExp ( repoOwner ) ;
1141+ const escapedAuthority = escapeRegExp ( authority ) ;
1142+
1143+ const diffPattern = new RegExp (
1144+ `<a\\s+(?![^>]*data-permalink-processed)([^>]*?href="https?:\/\/${ escapedAuthority } \/${ escapedRepoOwner } \/${ escapedRepoName } \/pull\/${ prNumber } \/(?:files|changes)#diff-(?<diffHash>[a-f0-9]{64})(?:R(?<startLine>\\d+)(?:-R(?<endLine>\\d+))?)?"[^>]*?)>(?<linkText>[^<]*?)<\/a>` ,
1145+ 'g'
1146+ ) ;
1147+
1148+ return await stringReplaceAsync ( bodyHTML , diffPattern , async (
1149+ fullMatch : string ,
1150+ attributes : string ,
1151+ diffHash : string ,
1152+ startLine : string | undefined ,
1153+ endLine : string | undefined ,
1154+ linkText : string
1155+ ) => {
1156+ try {
1157+ // Extract the original URL from attributes
1158+ const hrefMatch = attributes . match ( / h r e f = " ( [ ^ " ] + ) " / ) ;
1159+ const originalUrl = hrefMatch ? hrefMatch [ 1 ] : '' ;
1160+
1161+ // Look up filename from hash
1162+ const fileName = hashMap [ diffHash ] ;
1163+ if ( fileName ) {
1164+ // Hash found - add data attributes for diff handling and "(view on GitHub)" suffix
1165+ const startLineValue = startLine || '1' ;
1166+ const endLineValue = endLine || startLineValue ;
1167+ return `<a data-permalink-processed="true" ${ attributes } data-local-file="${ fileName } " data-start-line="${ startLineValue } " data-end-line="${ endLineValue } " data-link-type="diff">${ linkText } </a> (<a data-permalink-processed="true" href="${ originalUrl } ">view on GitHub</a>)` ;
1168+ }
1169+ } catch ( error ) {
1170+ // Failed to process - keep original link
1171+ }
1172+ return fullMatch ;
1173+ } ) ;
1174+ } catch ( error ) {
1175+ // Return original HTML if processing fails
1176+ return bodyHTML ;
1177+ }
10171178}
0 commit comments