11import type { GroupedEventDBScheme , ProjectDBScheme } from '@hawk.so/types' ;
22import { decodeUnsafeFields } from '../../../../lib/utils/unsafeFields' ;
3+ import TimeMs from '../../../../lib/utils/time' ;
34import type { IssueData } from '../GithubService' ;
45
6+ /**
7+ * Width used for padding date/time parts.
8+ */
9+ const DATE_TIME_PART_WIDTH = 2 ;
10+
11+ /**
12+ * Number of spaces used for JSON pretty-printing.
13+ */
14+ const JSON_INDENT_SPACES = 2 ;
15+
516/**
617 * Format date for display in GitHub issue
718 *
819 * @param timestamp - Unix timestamp in seconds
9- * @returns Formatted date string (e.g., "23 Feb 2025 14:40:21")
20+ * @returns { string } Formatted date string (e.g., "23 Feb 2025 14:40:21")
1021 */
1122function formatDate ( timestamp : number ) : string {
12- const date = new Date ( timestamp * 1000 ) ;
13- const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
23+ const date = new Date ( timestamp * TimeMs . SECOND ) ;
24+ const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
1425 const day = date . getUTCDate ( ) ;
1526 const month = months [ date . getUTCMonth ( ) ] ;
1627 const year = date . getUTCFullYear ( ) ;
17- const hours = date . getUTCHours ( ) . toString ( ) . padStart ( 2 , '0' ) ;
18- const minutes = date . getUTCMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
19- const seconds = date . getUTCSeconds ( ) . toString ( ) . padStart ( 2 , '0' ) ;
28+ const hours = date . getUTCHours ( ) . toString ( )
29+ . padStart ( DATE_TIME_PART_WIDTH , '0' ) ;
30+ const minutes = date . getUTCMinutes ( ) . toString ( )
31+ . padStart ( DATE_TIME_PART_WIDTH , '0' ) ;
32+ const seconds = date . getUTCSeconds ( ) . toString ( )
33+ . padStart ( DATE_TIME_PART_WIDTH , '0' ) ;
2034
2135 return `${ day } ${ month } ${ year } ${ hours } :${ minutes } :${ seconds } ` ;
2236}
@@ -25,12 +39,12 @@ function formatDate(timestamp: number): string {
2539 * Calculate days repeating from timestamp
2640 *
2741 * @param timestamp - Unix timestamp in seconds
28- * @returns Number of days since first occurrence
42+ * @returns { number } Number of days since first occurrence
2943 */
3044function calculateDaysRepeating ( timestamp : number ) : number {
3145 const now = Date . now ( ) ;
32- const eventTimestamp = timestamp * 1000 ;
33- const differenceInDays = ( now - eventTimestamp ) / ( 1000 * 3600 * 24 ) ;
46+ const eventTimestamp = timestamp * TimeMs . SECOND ;
47+ const differenceInDays = ( now - eventTimestamp ) / TimeMs . DAY ;
3448
3549 return Math . round ( differenceInDays ) ;
3650}
@@ -41,27 +55,35 @@ function calculateDaysRepeating(timestamp: number): number {
4155 *
4256 * @param sourceCode - Array of source code lines
4357 * @param errorLine - Line number where error occurred
44- * @returns Formatted diff string
58+ * @returns { string } Formatted diff string
4559 */
4660function formatSourceCodeAsDiff ( sourceCode : Array < { line : number ; content : string } > , errorLine : number ) : string {
4761 const lines : string [ ] = [ ] ;
4862
63+ /**
64+ * Use the widest line number among provided source lines and error line.
65+ * This keeps alignment correct for both small and large line numbers.
66+ */
67+ const maxLineNumber = sourceCode . reduce ( ( maxLine , current ) => {
68+ if ( current . line > maxLine ) {
69+ return current . line ;
70+ }
71+
72+ return maxLine ;
73+ } , errorLine ) ;
74+ const lineNumberWidth = String ( maxLineNumber ) . length ;
75+
4976 for ( const sourceLine of sourceCode ) {
50- const lineNumber = sourceLine . line . toString ( ) . padStart ( 3 , ' ' ) ;
77+ const lineNumber = sourceLine . line . toString ( ) . padStart ( lineNumberWidth , ' ' ) ;
5178 const isErrorLine = sourceLine . line === errorLine ;
5279 const prefix = isErrorLine ? '-' : ' ' ;
5380
5481 /**
55- * Escape HTML entities in content
82+ * Do not escape HTML here because content is rendered inside Markdown code block.
5683 */
57- const escapedContent = sourceLine . content
58- . replace ( / & / g, '&' )
59- . replace ( / < / g, '<' )
60- . replace ( / > / g, '>' )
61- . replace ( / " / g, '"' )
62- . replace ( / ' / g, ''' ) ;
63-
64- lines . push ( `${ prefix } ${ lineNumber } : ${ escapedContent } ` ) ;
84+ const content = sourceLine . content ?? '' ;
85+
86+ lines . push ( `${ prefix } ${ lineNumber } : ${ content } ` ) ;
6587 }
6688
6789 return lines . join ( '\n' ) ;
@@ -72,7 +94,7 @@ function formatSourceCodeAsDiff(sourceCode: Array<{ line: number; content: strin
7294 *
7395 * @param event - event to format issue for
7496 * @param project - project
75- * @returns Issue data for GitHub API
97+ * @returns { IssueData } Issue data for GitHub API
7698 */
7799export function formatIssueFromEvent ( event : GroupedEventDBScheme , project : ProjectDBScheme ) : IssueData {
78100 /**
@@ -195,7 +217,7 @@ export function formatIssueFromEvent(event: GroupedEventDBScheme, project: Proje
195217 if ( decodedEvent . payload . context ) {
196218 bodyParts . push ( '### Context' ) ;
197219 bodyParts . push ( '\n```json' ) ;
198- bodyParts . push ( JSON . stringify ( decodedEvent . payload . context , null , 2 ) ) ;
220+ bodyParts . push ( JSON . stringify ( decodedEvent . payload . context , null , JSON_INDENT_SPACES ) ) ;
199221 bodyParts . push ( '```' ) ;
200222 }
201223
@@ -205,7 +227,7 @@ export function formatIssueFromEvent(event: GroupedEventDBScheme, project: Proje
205227 if ( decodedEvent . payload . addons ) {
206228 bodyParts . push ( '\n### Addons' ) ;
207229 bodyParts . push ( '\n```json' ) ;
208- bodyParts . push ( JSON . stringify ( decodedEvent . payload . addons , null , 2 ) ) ;
230+ bodyParts . push ( JSON . stringify ( decodedEvent . payload . addons , null , JSON_INDENT_SPACES ) ) ;
209231 bodyParts . push ( '```' ) ;
210232 }
211233
0 commit comments