@@ -33,14 +33,84 @@ import {
3333 isWhitespaceInsensitive ,
3434} from './classifier' ;
3535import { normalizeText , getVisibleChildren , normalizeMustacheWhitespace , normalizeMustacheWhitespaceAll } from './utils' ;
36+ import type { CustomCodeTagConfig } from '../customCodeTags' ;
37+ import { getAttributeValue } from '../customCodeTags' ;
3638
3739export interface FormatterContext {
3840 document : TextDocument ;
3941 customCodeTags ?: Set < string > ;
42+ customCodeTagConfigs ?: Map < string , CustomCodeTagConfig > ;
4043 embeddedFormatted ?: Map < number , string > ;
4144 mustacheSpaces ?: boolean ;
4245}
4346
47+ /**
48+ * Check if an attribute value is truthy (not null, empty, "false", or "0").
49+ */
50+ export function isAttributeTruthy ( value : string | null ) : boolean {
51+ if ( value === null || value === '' || value === 'false' || value === '0' ) {
52+ return false ;
53+ }
54+ return true ;
55+ }
56+
57+ /**
58+ * Dedent content by stripping leading/trailing empty lines and removing the
59+ * minimum common indentation from all non-empty lines.
60+ */
61+ export function dedentContent ( rawContent : string ) : string {
62+ const lines = rawContent . split ( '\n' ) ;
63+
64+ // Strip leading empty lines
65+ while ( lines . length > 0 && lines [ 0 ] . trim ( ) === '' ) {
66+ lines . shift ( ) ;
67+ }
68+ // Strip trailing empty lines
69+ while ( lines . length > 0 && lines [ lines . length - 1 ] . trim ( ) === '' ) {
70+ lines . pop ( ) ;
71+ }
72+
73+ if ( lines . length === 0 ) return '' ;
74+
75+ // Find minimum indentation across non-empty lines
76+ let minIndent = Infinity ;
77+ for ( const l of lines ) {
78+ if ( l . trim ( ) === '' ) continue ;
79+ const match = l . match ( / ^ ( \s * ) / ) ;
80+ if ( match && match [ 1 ] . length < minIndent ) {
81+ minIndent = match [ 1 ] . length ;
82+ }
83+ }
84+ if ( minIndent === Infinity ) minIndent = 0 ;
85+
86+ // Strip common indent
87+ return lines . map ( l => l . trim ( ) === '' ? '' : l . slice ( minIndent ) ) . join ( '\n' ) ;
88+ }
89+
90+ /**
91+ * Resolve whether a custom code tag's content should be indented.
92+ */
93+ function resolveIndentMode (
94+ node : SyntaxNode ,
95+ config : CustomCodeTagConfig
96+ ) : boolean {
97+ const mode = config . indent ?? 'never' ;
98+ if ( mode === 'never' ) return false ;
99+ if ( mode === 'always' ) return true ;
100+ // mode === 'attribute'
101+ if ( ! config . indentAttribute ) return false ;
102+ const value = getAttributeValue ( node , config . indentAttribute ) ;
103+ return isAttributeTruthy ( value ) ;
104+ }
105+
106+ function getTagNameFromStartTag ( startTag : SyntaxNode ) : string | null {
107+ for ( let i = 0 ; i < startTag . childCount ; i ++ ) {
108+ const child = startTag . child ( i ) ;
109+ if ( child ?. type === 'html_tag_name' ) return child . text . toLowerCase ( ) ;
110+ }
111+ return null ;
112+ }
113+
44114function mustacheText ( raw : string , context : FormatterContext ) : string {
45115 if ( context . mustacheSpaces !== undefined ) {
46116 return normalizeMustacheWhitespace ( raw , context . mustacheSpaces ) ;
@@ -175,9 +245,39 @@ export function formatHtmlElement(node: SyntaxNode, context: FormatterContext):
175245
176246 // Handle content
177247 if ( preserveContent ) {
178- // Use raw document text to preserve all whitespace, since tree-sitter
179- // text nodes strip boundary whitespace from regular html_element children
180- if ( startTag && endTag ) {
248+ // Check if this custom code tag should be indented
249+ const tagNameLower = startTag ? getTagNameFromStartTag ( startTag ) : null ;
250+ const tagConfig = tagNameLower ? context . customCodeTagConfigs ?. get ( tagNameLower ) : undefined ;
251+ const shouldIndent = tagConfig ? resolveIndentMode ( node , tagConfig ) : false ;
252+
253+ if ( shouldIndent && startTag && endTag ) {
254+ const rawContent = context . document . getText ( ) . slice (
255+ startTag . endIndex ,
256+ endTag . startIndex
257+ ) ;
258+ const dedented = dedentContent ( rawContent ) ;
259+ if ( dedented . length > 0 ) {
260+ const contentLines = dedented . split ( '\n' ) ;
261+ const lineDocs : Doc [ ] = [ ] ;
262+ for ( let j = 0 ; j < contentLines . length ; j ++ ) {
263+ if ( j > 0 ) {
264+ if ( contentLines [ j ] === '' ) {
265+ // Empty line: literal \n avoids indentation from the printer
266+ lineDocs . push ( '\n' ) ;
267+ } else {
268+ lineDocs . push ( hardline ) ;
269+ }
270+ }
271+ if ( contentLines [ j ] !== '' ) {
272+ lineDocs . push ( text ( contentLines [ j ] ) ) ;
273+ }
274+ }
275+ parts . push ( indent ( concat ( [ hardline , ...lineDocs ] ) ) ) ;
276+ parts . push ( hardline ) ;
277+ }
278+ } else if ( startTag && endTag ) {
279+ // Use raw document text to preserve all whitespace, since tree-sitter
280+ // text nodes strip boundary whitespace from regular html_element children
181281 const rawContent = context . document . getText ( ) . slice (
182282 startTag . endIndex ,
183283 endTag . startIndex
@@ -306,7 +406,37 @@ export function formatScriptStyleElement(
306406 }
307407 } else {
308408 // Fallback: preserve raw content as-is (also used for html_raw_element)
309- parts . push ( text ( child . text ) ) ;
409+ // Check if this is a custom code tag that should be indented
410+ if ( node . type === 'html_raw_element' ) {
411+ const startTagNode = node . child ( 0 ) ;
412+ const tagNameLower = startTagNode ?. type === 'html_start_tag' ? getTagNameFromStartTag ( startTagNode ) : null ;
413+ const tagConfig = tagNameLower ? context . customCodeTagConfigs ?. get ( tagNameLower ) : undefined ;
414+ if ( tagConfig && resolveIndentMode ( node , tagConfig ) ) {
415+ const dedented = dedentContent ( child . text ) ;
416+ if ( dedented . length > 0 ) {
417+ const contentLines = dedented . split ( '\n' ) ;
418+ const lineDocs : Doc [ ] = [ ] ;
419+ for ( let j = 0 ; j < contentLines . length ; j ++ ) {
420+ if ( j > 0 ) {
421+ if ( contentLines [ j ] === '' ) {
422+ lineDocs . push ( '\n' ) ;
423+ } else {
424+ lineDocs . push ( hardline ) ;
425+ }
426+ }
427+ if ( contentLines [ j ] !== '' ) {
428+ lineDocs . push ( text ( contentLines [ j ] ) ) ;
429+ }
430+ }
431+ parts . push ( indent ( concat ( [ hardline , ...lineDocs ] ) ) ) ;
432+ parts . push ( hardline ) ;
433+ }
434+ } else {
435+ parts . push ( text ( child . text ) ) ;
436+ }
437+ } else {
438+ parts . push ( text ( child . text ) ) ;
439+ }
310440 }
311441 }
312442 }
0 commit comments