@@ -32,7 +32,7 @@ import {
3232 getCSSDisplay ,
3333 isWhitespaceInsensitive ,
3434} from './classifier' ;
35- import { normalizeText , getVisibleChildren , normalizeMustacheWhitespace , normalizeMustacheWhitespaceAll } from './utils' ;
35+ import { normalizeText , getVisibleChildren , normalizeMustacheWhitespace , normalizeMustacheWhitespaceAll , getIgnoreDirective } from './utils' ;
3636import type { CustomCodeTagConfig } from '../customCodeTags' ;
3737import { getAttributeValue } from '../customCodeTags' ;
3838
@@ -298,8 +298,18 @@ export function formatHtmlElement(node: SyntaxNode, context: FormatterContext):
298298 }
299299 } else if ( ! isBlock && ! hasHtmlElementChildren ) {
300300 // Inline element with only text/interpolation content - keep tight
301+ // Preserve whitespace gaps between sibling nodes (e.g. space between
302+ // mustache_interpolation and text that tree-sitter puts in the gap)
303+ let prevEnd = startTag ? startTag . endIndex : - 1 ;
301304 for ( const child of contentNodes ) {
305+ if ( prevEnd >= 0 && child . startIndex > prevEnd ) {
306+ const gap = context . document . getText ( ) . slice ( prevEnd , child . startIndex ) ;
307+ if ( / \s / . test ( gap ) ) {
308+ parts . push ( text ( ' ' ) ) ;
309+ }
310+ }
302311 parts . push ( formatNode ( child , context ) ) ;
312+ prevEnd = child . endIndex ;
303313 }
304314 } else {
305315 // Block element or inline-with-block-children: use hardline + indent
@@ -435,7 +445,32 @@ export function formatScriptStyleElement(
435445 parts . push ( text ( child . text ) ) ;
436446 }
437447 } else {
438- parts . push ( text ( child . text ) ) ;
448+ // Script/style fallback: dedent and re-emit with hardlines so the
449+ // printer can apply proper indentation from parent context.
450+ const dedented = dedentContent ( child . text ) ;
451+ if ( dedented . length > 0 ) {
452+ const contentLines = dedented . split ( '\n' ) ;
453+ if ( contentLines . length === 1 ) {
454+ // Single-line content: keep inline
455+ parts . push ( text ( contentLines [ 0 ] ) ) ;
456+ } else {
457+ const lineDocs : Doc [ ] = [ ] ;
458+ for ( let j = 0 ; j < contentLines . length ; j ++ ) {
459+ if ( j > 0 ) {
460+ if ( contentLines [ j ] === '' ) {
461+ lineDocs . push ( '\n' ) ;
462+ } else {
463+ lineDocs . push ( hardline ) ;
464+ }
465+ }
466+ if ( contentLines [ j ] !== '' ) {
467+ lineDocs . push ( text ( contentLines [ j ] ) ) ;
468+ }
469+ }
470+ parts . push ( indent ( concat ( [ hardline , ...lineDocs ] ) ) ) ;
471+ parts . push ( hardline ) ;
472+ }
473+ }
439474 }
440475 }
441476 }
@@ -716,25 +751,115 @@ export function formatBlockChildren(
716751 let lastNodeEnd = - 1 ;
717752 let pendingBlankLine = false ;
718753 let blankLineBeforeCurrentLine = false ;
754+ let ignoreNext = false ;
755+ let inIgnoreRegion = false ;
756+ let ignoreRegionStartIndex = - 1 ;
719757
720758 for ( let i = 0 ; i < nodes . length ; i ++ ) {
721759 const node = nodes [ i ] ;
722760
723- const treatAsBlock = shouldTreatAsBlock ( node , i , nodes ) ;
724-
725- // Check for whitespace between nodes in original document
726- if ( lastNodeEnd >= 0 && node . startIndex > lastNodeEnd ) {
761+ // Detect blank lines in gap between nodes (before directive handling)
762+ if ( lastNodeEnd >= 0 && node . startIndex > lastNodeEnd && ! inIgnoreRegion ) {
727763 const gap = context . document . getText ( ) . slice ( lastNodeEnd , node . startIndex ) ;
728- const prevNode = nodes [ i - 1 ] ;
729- const prevTreatAsBlock = shouldTreatAsBlock ( prevNode , i - 1 , nodes ) ;
730-
731- // Detect blank lines (≥2 newlines) in any gap between nodes
732764 const newlineCount = ( gap . match ( / \n / g) || [ ] ) . length ;
733765 if ( newlineCount >= 2 ) {
734766 pendingBlankLine = true ;
735767 }
768+ }
769+
770+ const directive = getIgnoreDirective ( node ) ;
771+
772+ // --- Ignore directive handling ---
773+
774+ // ignore-end: close a region
775+ if ( directive === 'ignore-end' && inIgnoreRegion ) {
776+ // Flush any pending inline content
777+ if ( currentLine . length > 0 ) {
778+ const lineContent = trimDoc ( inlineContentToFill ( currentLine ) ) ;
779+ if ( hasDocContent ( lineContent ) ) {
780+ lines . push ( { doc : lineContent , blankLineBefore : blankLineBeforeCurrentLine } ) ;
781+ }
782+ currentLine = [ ] ;
783+ blankLineBeforeCurrentLine = false ;
784+ }
785+ // Emit raw text from region start to this comment, trimming boundary newlines
786+ const rawText = context . document . getText ( ) . slice ( ignoreRegionStartIndex , node . startIndex )
787+ . replace ( / ^ \n / , '' ) . replace ( / \n $ / , '' ) ;
788+ if ( rawText . length > 0 ) {
789+ lines . push ( { doc : text ( rawText ) , blankLineBefore : false } ) ;
790+ }
791+ // Emit the ignore-end comment itself
792+ const commentText = node . type === 'mustache_comment' ? mustacheText ( node . text , context ) : node . text ;
793+ lines . push ( { doc : text ( commentText ) , blankLineBefore : false } ) ;
794+ inIgnoreRegion = false ;
795+ ignoreRegionStartIndex = - 1 ;
796+ lastNodeEnd = node . endIndex ;
797+ continue ;
798+ }
799+
800+ // Inside ignore region: skip (content captured as raw text at ignore-end)
801+ if ( inIgnoreRegion ) {
802+ lastNodeEnd = node . endIndex ;
803+ continue ;
804+ }
805+
806+ // ignore-start: begin a region
807+ if ( directive === 'ignore-start' ) {
808+ if ( currentLine . length > 0 ) {
809+ const lineContent = trimDoc ( inlineContentToFill ( currentLine ) ) ;
810+ if ( hasDocContent ( lineContent ) ) {
811+ lines . push ( { doc : lineContent , blankLineBefore : blankLineBeforeCurrentLine } ) ;
812+ }
813+ currentLine = [ ] ;
814+ blankLineBeforeCurrentLine = false ;
815+ }
816+ const commentText = node . type === 'mustache_comment' ? mustacheText ( node . text , context ) : node . text ;
817+ lines . push ( { doc : text ( commentText ) , blankLineBefore : pendingBlankLine } ) ;
818+ pendingBlankLine = false ;
819+ inIgnoreRegion = true ;
820+ ignoreRegionStartIndex = node . endIndex ;
821+ lastNodeEnd = node . endIndex ;
822+ continue ;
823+ }
824+
825+ // ignore (next-node): emit the comment, set flag
826+ if ( directive === 'ignore' ) {
827+ if ( currentLine . length > 0 ) {
828+ const lineContent = trimDoc ( inlineContentToFill ( currentLine ) ) ;
829+ if ( hasDocContent ( lineContent ) ) {
830+ lines . push ( { doc : lineContent , blankLineBefore : blankLineBeforeCurrentLine } ) ;
831+ }
832+ currentLine = [ ] ;
833+ blankLineBeforeCurrentLine = false ;
834+ }
835+ const commentText = node . type === 'mustache_comment' ? mustacheText ( node . text , context ) : node . text ;
836+ lines . push ( { doc : text ( commentText ) , blankLineBefore : pendingBlankLine } ) ;
837+ pendingBlankLine = false ;
838+ ignoreNext = true ;
839+ lastNodeEnd = node . endIndex ;
840+ continue ;
841+ }
842+
843+ // Ignored next-node: emit raw text, clear flag
844+ if ( ignoreNext ) {
845+ lines . push ( { doc : text ( node . text ) , blankLineBefore : pendingBlankLine } ) ;
846+ pendingBlankLine = false ;
847+ ignoreNext = false ;
848+ lastNodeEnd = node . endIndex ;
849+ continue ;
850+ }
851+
852+ // ignore-end without ignore-start: treat as normal comment (fall through)
853+
854+ const treatAsBlock = shouldTreatAsBlock ( node , i , nodes ) ;
855+
856+ // Check for whitespace between nodes in original document (inline gap handling)
857+ if ( lastNodeEnd >= 0 && node . startIndex > lastNodeEnd ) {
858+ const prevNode = nodes [ i - 1 ] ;
859+ const prevTreatAsBlock = shouldTreatAsBlock ( prevNode , i - 1 , nodes ) ;
736860
737861 if ( ! prevTreatAsBlock && ! treatAsBlock ) {
862+ const gap = context . document . getText ( ) . slice ( lastNodeEnd , node . startIndex ) ;
738863 if ( / \s / . test ( gap ) ) {
739864 currentLine . push ( line ) ;
740865 }
@@ -875,6 +1000,16 @@ export function formatBlockChildren(
8751000 lastNodeEnd = node . endIndex ;
8761001 }
8771002
1003+ // Handle unterminated ignore region: emit remaining raw text
1004+ if ( inIgnoreRegion && nodes . length > 0 ) {
1005+ const lastNode = nodes [ nodes . length - 1 ] ;
1006+ const rawText = context . document . getText ( ) . slice ( ignoreRegionStartIndex , lastNode . endIndex )
1007+ . replace ( / ^ \n / , '' ) ;
1008+ if ( rawText . length > 0 ) {
1009+ lines . push ( { doc : text ( rawText ) , blankLineBefore : false } ) ;
1010+ }
1011+ }
1012+
8781013 // Flush remaining inline content
8791014 if ( currentLine . length > 0 ) {
8801015 const lineContent = trimDoc ( inlineContentToFill ( currentLine ) ) ;
0 commit comments