@@ -169,12 +169,14 @@ class Compiler {
169169 rawPrelude ?: string ,
170170 ) {
171171 if ( this . identity && rawPrelude ) {
172- return (
172+ this . indent ( 1 ) ;
173+ const result =
173174 this . emit ( rawPrelude , position ) +
174175 this . emit ( '{' ) +
175- this . mapVisit ( this . filterEmptyRules ( rules ) ) +
176- this . emit ( '}' )
177- ) ;
176+ this . identityVisitBlock ( this . filterEmptyRules ( rules ) , 'rules' ) +
177+ this . emit ( '}' ) ;
178+ this . indent ( - 1 ) ;
179+ return result ;
178180 }
179181 const filteredRules = this . filterEmptyRules ( this . stripWhitespace ( rules ) ) ;
180182 if ( this . compress ) {
@@ -203,12 +205,14 @@ class Compiler {
203205 rawPrelude ?: string ,
204206 ) {
205207 if ( this . identity && rawPrelude ) {
206- return (
208+ this . indent ( 1 ) ;
209+ const result =
207210 this . emit ( rawPrelude , position ) +
208211 this . emit ( '{' ) +
209- this . mapVisit ( declarations ) +
210- this . emit ( '}' )
211- ) ;
212+ this . identityVisitBlock ( declarations , 'decls' ) +
213+ this . emit ( '}' ) ;
214+ this . indent ( - 1 ) ;
215+ return result ;
212216 }
213217 const stripped = this . stripWhitespace ( declarations ) ;
214218 if ( this . compress ) {
@@ -253,7 +257,7 @@ class Compiler {
253257 // Fallback to beautified when preserveFormatting was not used
254258 return this . stylesheet ( node ) ;
255259 }
256- return this . filterEmptyRules ( rules ) . map ( this . visit , this ) . join ( ' ') ;
260+ return this . identityVisitBlock ( rules as CssAllNodesAST [ ] , 'stylesheet ') ;
257261 }
258262
259263 /**
@@ -293,6 +297,111 @@ class Compiler {
293297 } ) ;
294298 }
295299
300+ /**
301+ * Check whether a node was newly created (not parsed from source).
302+ * New nodes lack the raw formatting properties set by the parser.
303+ */
304+ private isNewNode ( node : CssAllNodesAST ) : boolean {
305+ switch ( node . type ) {
306+ case CssTypes . whitespace :
307+ return false ;
308+ case CssTypes . comment :
309+ return ! ( node as CssCommonPositionAST ) . position ;
310+ case CssTypes . declaration :
311+ return ( node as CssDeclarationAST ) . rawBetween == null ;
312+ case CssTypes . rule :
313+ return ( node as CssRuleAST ) . rawPrelude == null ;
314+ default :
315+ if ( 'rawPrelude' in node )
316+ return ! ( node as { rawPrelude ?: string } ) . rawPrelude ;
317+ if ( 'rawSource' in node )
318+ return ! ( node as { rawSource ?: string } ) . rawSource ;
319+ return ! ( node as CssCommonPositionAST ) . position ;
320+ }
321+ }
322+
323+ /**
324+ * Visit block children in identity mode, formatting newly added nodes
325+ * with beautified output while preserving original formatting for
326+ * existing nodes.
327+ *
328+ * @param nodes - The child nodes to visit
329+ * @param context - 'decls' for declaration blocks, 'rules' for nested
330+ * rule blocks (e.g. @media), 'stylesheet' for top-level
331+ */
332+ private identityVisitBlock (
333+ nodes : Array < CssAllNodesAST > ,
334+ context : 'decls' | 'rules' | 'stylesheet' ,
335+ ) : string {
336+ const filtered = this . filterEmptyRules ( nodes ) ;
337+
338+ // Fast path: no new nodes – visit normally
339+ if ( ! filtered . some ( ( n ) => this . isNewNode ( n ) ) ) {
340+ return this . mapVisit ( filtered ) ;
341+ }
342+
343+ let buf = '' ;
344+ const needsDoubleNewline = context === 'rules' || context === 'stylesheet' ;
345+
346+ for ( let i = 0 ; i < filtered . length ; i ++ ) {
347+ const node = filtered [ i ] ;
348+
349+ if ( ! this . isNewNode ( node ) ) {
350+ buf += this . visit ( node ) ;
351+ continue ;
352+ }
353+
354+ // ── New node: add proper separator before it ──
355+
356+ // Check whether the buffer already ends with \n (possibly followed
357+ // by spaces that were originally the closing-brace indent). If so,
358+ // trim those trailing spaces – the beautified fallback of the new
359+ // node will supply the correct indentation.
360+ const lastNl = buf . lastIndexOf ( '\n' ) ;
361+ const hasTrailingNewline =
362+ lastNl >= 0 && buf . slice ( lastNl + 1 ) . trim ( ) === '' ;
363+
364+ if ( hasTrailingNewline ) {
365+ buf = buf . slice ( 0 , lastNl + 1 ) ;
366+ } else if ( buf . length === 0 && context !== 'stylesheet' ) {
367+ // First child in a block – add newline after opening brace
368+ buf += '\n' ;
369+ } else if ( buf . length > 0 ) {
370+ buf += '\n' ;
371+ }
372+
373+ if ( needsDoubleNewline && buf . length > 1 && ! buf . endsWith ( '\n\n' ) ) {
374+ buf += '\n' ;
375+ }
376+
377+ // Visit the node (uses beautified fallback since raw props are absent)
378+ const str = this . visit ( node ) ;
379+ if ( str ) buf += str ;
380+ }
381+
382+ // Add trailing newline + closing-brace indent when the last meaningful
383+ // node is new (blocks only, not the top-level stylesheet)
384+ if ( context !== 'stylesheet' ) {
385+ const lastMeaningful = [ ...filtered ]
386+ . reverse ( )
387+ . find ( ( n ) => n . type !== CssTypes . whitespace ) ;
388+ if (
389+ lastMeaningful &&
390+ this . isNewNode ( lastMeaningful ) &&
391+ buf . length > 0 &&
392+ ! buf . endsWith ( '\n' )
393+ ) {
394+ buf += '\n' ;
395+ // Indent for the closing brace (one level less than current)
396+ if ( this . level > 2 ) {
397+ buf += this . indentation . repeat ( this . level - 2 ) ;
398+ }
399+ }
400+ }
401+
402+ return buf ;
403+ }
404+
296405 /**
297406 * Visit whitespace node.
298407 */
@@ -311,6 +420,9 @@ class Compiler {
311420 if ( this . compress ) {
312421 return this . emit ( '' , node . position ) ;
313422 }
423+ if ( this . identity && node . position ) {
424+ return this . emit ( `/*${ node . comment } */` , node . position ) ;
425+ }
314426 return this . emit ( `${ this . indent ( ) } /*${ node . comment } */` , node . position ) ;
315427 }
316428
@@ -331,12 +443,17 @@ class Compiler {
331443 */
332444 layer ( node : CssLayerAST ) {
333445 if ( this . identity && node . rawPrelude && node . rules ) {
334- return (
446+ this . indent ( 1 ) ;
447+ const result =
335448 this . emit ( node . rawPrelude , node . position ) +
336449 this . emit ( '{' ) +
337- this . mapVisit ( this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ) +
338- this . emit ( '}' )
339- ) ;
450+ this . identityVisitBlock (
451+ this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ,
452+ 'rules' ,
453+ ) +
454+ this . emit ( '}' ) ;
455+ this . indent ( - 1 ) ;
456+ return result ;
340457 }
341458 if ( this . identity && ! node . rules && node . rawSource ) {
342459 return this . emit ( node . rawSource , node . position ) ;
@@ -388,12 +505,14 @@ class Compiler {
388505 document ( node : CssDocumentAST ) {
389506 const doc = `@${ node . vendor || '' } document ${ node . document } ` ;
390507 if ( this . identity && node . rawPrelude ) {
391- return (
508+ this . indent ( 1 ) ;
509+ const result =
392510 this . emit ( node . rawPrelude , node . position ) +
393511 this . emit ( '{' ) +
394- this . mapVisit ( this . filterEmptyRules ( node . rules ) ) +
395- this . emit ( '}' )
396- ) ;
512+ this . identityVisitBlock ( this . filterEmptyRules ( node . rules ) , 'rules' ) +
513+ this . emit ( '}' ) ;
514+ this . indent ( - 1 ) ;
515+ return result ;
397516 }
398517 const rules = this . stripWhitespace ( node . rules ) ;
399518 if ( this . compress ) {
@@ -461,12 +580,14 @@ class Compiler {
461580 */
462581 keyframes ( node : CssKeyframesAST ) {
463582 if ( this . identity && node . rawPrelude ) {
464- return (
583+ this . indent ( 1 ) ;
584+ const result =
465585 this . emit ( node . rawPrelude , node . position ) +
466586 this . emit ( '{' ) +
467- this . mapVisit ( node . keyframes ) +
468- this . emit ( '}' )
469- ) ;
587+ this . identityVisitBlock ( node . keyframes , 'rules' ) +
588+ this . emit ( '}' ) ;
589+ this . indent ( - 1 ) ;
590+ return result ;
470591 }
471592 const frames = this . stripWhitespace ( node . keyframes ) ;
472593 if ( this . compress ) {
@@ -494,12 +615,14 @@ class Compiler {
494615 keyframe ( node : CssKeyframeAST ) {
495616 const decls = node . declarations ;
496617 if ( this . identity && node . rawPrelude ) {
497- return (
618+ this . indent ( 1 ) ;
619+ const result =
498620 this . emit ( node . rawPrelude , node . position ) +
499621 this . emit ( '{' ) +
500- this . mapVisit ( decls ) +
501- this . emit ( '}' )
502- ) ;
622+ this . identityVisitBlock ( decls , 'decls' ) +
623+ this . emit ( '}' ) ;
624+ this . indent ( - 1 ) ;
625+ return result ;
503626 }
504627 const stripped = this . stripWhitespace ( decls ) ;
505628 if ( this . compress ) {
@@ -525,12 +648,14 @@ class Compiler {
525648 */
526649 page ( node : CssPageAST ) {
527650 if ( this . identity && node . rawPrelude ) {
528- return (
651+ this . indent ( 1 ) ;
652+ const result =
529653 this . emit ( node . rawPrelude , node . position ) +
530654 this . emit ( '{' ) +
531- this . mapVisit ( node . declarations ) +
532- this . emit ( '}' )
533- ) ;
655+ this . identityVisitBlock ( node . declarations , 'decls' ) +
656+ this . emit ( '}' ) ;
657+ this . indent ( - 1 ) ;
658+ return result ;
534659 }
535660 const decls = this . stripWhitespace ( node . declarations ) ;
536661 if ( this . compress ) {
@@ -560,12 +685,14 @@ class Compiler {
560685 */
561686 pageMarginBox ( node : CssPageMarginBoxAST ) {
562687 if ( this . identity && node . rawPrelude ) {
563- return (
688+ this . indent ( 1 ) ;
689+ const result =
564690 this . emit ( node . rawPrelude , node . position ) +
565691 this . emit ( '{' ) +
566- this . mapVisit ( node . declarations ) +
567- this . emit ( '}' )
568- ) ;
692+ this . identityVisitBlock ( node . declarations , 'decls' ) +
693+ this . emit ( '}' ) ;
694+ this . indent ( - 1 ) ;
695+ return result ;
569696 }
570697 const decls = this . stripWhitespace ( node . declarations ) ;
571698 if ( this . compress ) {
@@ -603,12 +730,14 @@ class Compiler {
603730 */
604731 host ( node : CssHostAST ) {
605732 if ( this . identity && node . rawPrelude ) {
606- return (
733+ this . indent ( 1 ) ;
734+ const result =
607735 this . emit ( node . rawPrelude , node . position ) +
608736 this . emit ( '{' ) +
609- this . mapVisit ( this . filterEmptyRules ( node . rules ) ) +
610- this . emit ( '}' )
611- ) ;
737+ this . identityVisitBlock ( this . filterEmptyRules ( node . rules ) , 'rules' ) +
738+ this . emit ( '}' ) ;
739+ this . indent ( - 1 ) ;
740+ return result ;
612741 }
613742 const rules = this . stripWhitespace ( node . rules ) ;
614743 if ( this . compress ) {
@@ -681,12 +810,14 @@ class Compiler {
681810 */
682811 scope ( node : CssScopeAST ) {
683812 if ( this . identity && node . rawPrelude ) {
684- return (
813+ this . indent ( 1 ) ;
814+ const result =
685815 this . emit ( node . rawPrelude , node . position ) +
686816 this . emit ( '{' ) +
687- this . mapVisit ( this . filterEmptyRules ( node . rules ) ) +
688- this . emit ( '}' )
689- ) ;
817+ this . identityVisitBlock ( this . filterEmptyRules ( node . rules ) , 'rules' ) +
818+ this . emit ( '}' ) ;
819+ this . indent ( - 1 ) ;
820+ return result ;
690821 }
691822 const prelude = node . scope ? ` ${ node . scope } ` : '' ;
692823 return this . rulesBlock (
@@ -725,12 +856,17 @@ class Compiler {
725856 */
726857 genericAtRule ( node : CssGenericAtRuleAST ) {
727858 if ( this . identity && node . rawPrelude && node . rules ) {
728- return (
859+ this . indent ( 1 ) ;
860+ const result =
729861 this . emit ( node . rawPrelude , node . position ) +
730862 this . emit ( '{' ) +
731- this . mapVisit ( this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ) +
732- this . emit ( '}' )
733- ) ;
863+ this . identityVisitBlock (
864+ this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ,
865+ 'rules' ,
866+ ) +
867+ this . emit ( '}' ) ;
868+ this . indent ( - 1 ) ;
869+ return result ;
734870 }
735871 const prelude = node . prelude ? ` ${ node . prelude } ` : '' ;
736872 const rules = node . rules
@@ -772,12 +908,14 @@ class Compiler {
772908 const decls = node . declarations ;
773909
774910 if ( this . identity && node . rawPrelude ) {
775- return (
911+ this . indent ( 1 ) ;
912+ const result =
776913 this . emit ( node . rawPrelude , node . position ) +
777914 this . emit ( '{' ) +
778- this . mapVisit ( decls ) +
779- this . emit ( '}' )
780- ) ;
915+ this . identityVisitBlock ( decls , 'decls' ) +
916+ this . emit ( '}' ) ;
917+ this . indent ( - 1 ) ;
918+ return result ;
781919 }
782920
783921 const stripped = this . stripWhitespace ( decls ) ;
0 commit comments