@@ -104,6 +104,70 @@ export const CLOSING_TAG_REGEX = /<\/([a-zA-Z][a-zA-Z0-9.-]*)>/g;
104104// String Safety Checks
105105// ============================================
106106
107+ /**
108+ * Check if a position in code is inside a TypeScript type context.
109+ * This prevents injecting attributes into TypeScript generics like:
110+ * - React.MouseEvent<HTMLDivElement> (type annotation)
111+ * - Array<string> (generic parameter)
112+ * - T extends HTMLElement (extends clause)
113+ * - Promise<Result<T>> (nested generics)
114+ *
115+ * @param code - The full source code
116+ * @param position - Character position to check (position of '<')
117+ * @returns True if inside a TypeScript type context
118+ */
119+ export function isInsideTypeScriptTypeContext ( code : string , position : number ) : boolean {
120+ // Look backwards from position to find context
121+ const beforeMatch = code . substring ( Math . max ( 0 , position - 100 ) , position ) ;
122+
123+ // Pattern 1: Type annotation - "event: React.MouseEvent<" or "value: Array<"
124+ // Look for ": TypeName<" pattern (colon followed by identifier then our position)
125+ if ( / : \s * [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ . < > ] * $ / . test ( beforeMatch ) ) {
126+ return true ;
127+ }
128+
129+ // Pattern 2: Generic parameter - "Promise<Result<" (nested angle brackets)
130+ // Count unclosed < before this position
131+ let angleBracketDepth = 0 ;
132+ for ( let i = 0 ; i < beforeMatch . length ; i ++ ) {
133+ const char = beforeMatch [ i ] ;
134+ if ( char === '<' ) {
135+ angleBracketDepth ++ ;
136+ } else if ( char === '>' ) {
137+ angleBracketDepth = Math . max ( 0 , angleBracketDepth - 1 ) ;
138+ }
139+ }
140+ if ( angleBracketDepth > 0 ) {
141+ return true ;
142+ }
143+
144+ // Pattern 3: Extends clause - "T extends HTMLElement" or "interface Foo extends Bar<"
145+ if ( / \b e x t e n d s \s + [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ . < > ] * $ / . test ( beforeMatch ) ) {
146+ return true ;
147+ }
148+
149+ // Pattern 4: Type parameter declaration - "function foo<T extends "
150+ if ( / [ < , ] \s * [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ ] * \s + e x t e n d s \s + [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ . < > ] * $ / . test ( beforeMatch ) ) {
151+ return true ;
152+ }
153+
154+ // Pattern 5: Return type annotation - "): Promise<" or "=> Array<"
155+ if ( / [ ) : ] \s * [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ . < > ] * $ / . test ( beforeMatch ) ) {
156+ // Extra check: make sure it's not JSX return like "return <div>"
157+ // JSX returns usually have whitespace/newline after return keyword
158+ if ( ! / \b r e t u r n \s + $ / . test ( beforeMatch ) && ! / = > \s * $ / . test ( beforeMatch . replace ( / [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ . < > ] * $ / , '' ) ) ) {
159+ return true ;
160+ }
161+ }
162+
163+ // Pattern 6: Type assertion - "as HTMLDivElement" or "<HTMLDivElement>"
164+ if ( / \b a s \s + [ A - Z a - z _ $ ] [ A - Z a - z 0 - 9 _ $ . < > ] * $ / . test ( beforeMatch ) ) {
165+ return true ;
166+ }
167+
168+ return false ;
169+ }
170+
107171/**
108172 * Check if a position in code is inside a string literal or template literal.
109173 * This prevents modifying HTML code that's displayed as text content
@@ -278,7 +342,9 @@ export function findMatchingClosingTag(allMatches: TagMatch[], openingPos: numbe
278342 foundOpening = true ;
279343 continue ;
280344 }
281- if ( ! foundOpening ) continue ;
345+ if ( ! foundOpening ) {
346+ continue ;
347+ }
282348
283349 if ( match . tagName === tagName ) {
284350 if ( match . type === 'opening' ) {
@@ -356,6 +422,12 @@ export function parseElements(code: string, options: ParseOptions): ElementRepla
356422 continue ;
357423 }
358424
425+ // TYPESCRIPT: Skip if inside TypeScript type context (generics, type annotations)
426+ // This prevents injecting into: React.MouseEvent<HTMLDivElement>, Array<string>, etc.
427+ if ( isInsideTypeScriptTypeContext ( code , matchStart ) ) {
428+ continue ;
429+ }
430+
359431 // Calculate start position
360432 const startPos = calculatePosition ( code , matchStart , baseLineOffset ) ;
361433
0 commit comments