@@ -4,6 +4,12 @@ import { getNodeText } from "@/utils/get-node-text";
44import { SHIKI_CLASSNAME } from "@/utils/shiki/constants" ;
55
66const lineIndentRegex = / ^ ( * ) / ;
7+ const closingStructureRegex = / ^ ( [ } \] ) ] | < \/ ) / ;
8+
9+ function getIndent ( line : string ) : number {
10+ const match = line . match ( lineIndentRegex ) ;
11+ return match ? match [ 1 ] . length : 0 ;
12+ }
713
814function findShikiClassName ( children : unknown ) : boolean {
915 if ( ! children || typeof children !== "object" ) {
@@ -45,26 +51,46 @@ function dedentCode(code: string): string {
4551 return code ;
4652 }
4753
48- const relevantLines = lines . slice ( 1 ) . filter ( ( line ) => line . trim ( ) !== "" ) ;
54+ const relevantLines = lines . filter ( ( line ) => line . trim ( ) !== "" ) ;
4955 if ( relevantLines . length === 0 ) {
5056 return code ;
5157 }
5258
53- const minIndent = Math . min (
54- ...relevantLines . map ( ( line ) => {
55- const match = line . match ( lineIndentRegex ) ;
56- return match ? match [ 1 ] . length : 0 ;
57- } )
58- ) ;
59+ const firstLine = relevantLines [ 0 ] ;
60+ const lastLine = relevantLines . at ( - 1 ) ?? firstLine ;
61+ const firstIndent = getIndent ( firstLine ) ;
62+ const lastIndent = getIndent ( lastLine ) ;
63+ // Detects template-literal pollution: the opening backtick strips the first
64+ // line's indent, leaving it at column 0 while the rest of the body retains
65+ // JSX whitespace. The matching closing structure ( }, ), ], </tag>) ends up
66+ // deeper than its opener, which is structurally invalid and a reliable signal
67+ // that the body should be dedented while the first line is left alone.
68+ const isTemplatePolluted =
69+ firstIndent < lastIndent && closingStructureRegex . test ( lastLine . trim ( ) ) ;
70+
71+ if ( isTemplatePolluted ) {
72+ const firstNonEmptyIndex = lines . findIndex ( ( line ) => line . trim ( ) !== "" ) ;
73+ const tail = relevantLines . slice ( 1 ) ;
74+ if ( tail . length === 0 ) {
75+ return code ;
76+ }
77+ const minIndent = Math . min ( ...tail . map ( getIndent ) ) ;
78+ if ( minIndent === 0 ) {
79+ return code ;
80+ }
81+ return lines
82+ . map ( ( line , i ) =>
83+ i <= firstNonEmptyIndex ? line : line . slice ( minIndent )
84+ )
85+ . join ( "\n" ) ;
86+ }
5987
88+ const minIndent = Math . min ( ...relevantLines . map ( getIndent ) ) ;
6089 if ( minIndent === 0 ) {
6190 return code ;
6291 }
6392
64- return [
65- lines [ 0 ] ,
66- ...lines . slice ( 1 ) . map ( ( line ) => line . slice ( minIndent ) ) ,
67- ] . join ( "\n" ) ;
93+ return lines . map ( ( line ) => line . slice ( minIndent ) ) . join ( "\n" ) ;
6894}
6995
7096function getCodeString (
0 commit comments