@@ -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+ this . indent ( - 1 ) ;
397+ buf += this . indent ( ) ;
398+ this . indent ( 1 ) ;
399+ }
400+ }
401+
402+ return buf ;
403+ }
404+
296405 /**
297406 * Visit whitespace node.
298407 */
@@ -311,7 +420,8 @@ class Compiler {
311420 if ( this . compress ) {
312421 return this . emit ( '' , node . position ) ;
313422 }
314- return this . emit ( `${ this . indent ( ) } /*${ node . comment } */` , node . position ) ;
423+ const indent = this . identity && node . position ? '' : this . indent ( ) ;
424+ return this . emit ( `${ indent } /*${ node . comment } */` , node . position ) ;
315425 }
316426
317427 /**
@@ -331,12 +441,17 @@ class Compiler {
331441 */
332442 layer ( node : CssLayerAST ) {
333443 if ( this . identity && node . rawPrelude && node . rules ) {
334- return (
444+ this . indent ( 1 ) ;
445+ const result =
335446 this . emit ( node . rawPrelude , node . position ) +
336447 this . emit ( '{' ) +
337- this . mapVisit ( this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ) +
338- this . emit ( '}' )
339- ) ;
448+ this . identityVisitBlock (
449+ this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ,
450+ 'rules' ,
451+ ) +
452+ this . emit ( '}' ) ;
453+ this . indent ( - 1 ) ;
454+ return result ;
340455 }
341456 if ( this . identity && ! node . rules && node . rawSource ) {
342457 return this . emit ( node . rawSource , node . position ) ;
@@ -388,12 +503,14 @@ class Compiler {
388503 document ( node : CssDocumentAST ) {
389504 const doc = `@${ node . vendor || '' } document ${ node . document } ` ;
390505 if ( this . identity && node . rawPrelude ) {
391- return (
506+ this . indent ( 1 ) ;
507+ const result =
392508 this . emit ( node . rawPrelude , node . position ) +
393509 this . emit ( '{' ) +
394- this . mapVisit ( this . filterEmptyRules ( node . rules ) ) +
395- this . emit ( '}' )
396- ) ;
510+ this . identityVisitBlock ( this . filterEmptyRules ( node . rules ) , 'rules' ) +
511+ this . emit ( '}' ) ;
512+ this . indent ( - 1 ) ;
513+ return result ;
397514 }
398515 const rules = this . stripWhitespace ( node . rules ) ;
399516 if ( this . compress ) {
@@ -461,12 +578,14 @@ class Compiler {
461578 */
462579 keyframes ( node : CssKeyframesAST ) {
463580 if ( this . identity && node . rawPrelude ) {
464- return (
581+ this . indent ( 1 ) ;
582+ const result =
465583 this . emit ( node . rawPrelude , node . position ) +
466584 this . emit ( '{' ) +
467- this . mapVisit ( node . keyframes ) +
468- this . emit ( '}' )
469- ) ;
585+ this . identityVisitBlock ( node . keyframes , 'rules' ) +
586+ this . emit ( '}' ) ;
587+ this . indent ( - 1 ) ;
588+ return result ;
470589 }
471590 const frames = this . stripWhitespace ( node . keyframes ) ;
472591 if ( this . compress ) {
@@ -494,12 +613,14 @@ class Compiler {
494613 keyframe ( node : CssKeyframeAST ) {
495614 const decls = node . declarations ;
496615 if ( this . identity && node . rawPrelude ) {
497- return (
616+ this . indent ( 1 ) ;
617+ const result =
498618 this . emit ( node . rawPrelude , node . position ) +
499619 this . emit ( '{' ) +
500- this . mapVisit ( decls ) +
501- this . emit ( '}' )
502- ) ;
620+ this . identityVisitBlock ( decls , 'decls' ) +
621+ this . emit ( '}' ) ;
622+ this . indent ( - 1 ) ;
623+ return result ;
503624 }
504625 const stripped = this . stripWhitespace ( decls ) ;
505626 if ( this . compress ) {
@@ -525,12 +646,14 @@ class Compiler {
525646 */
526647 page ( node : CssPageAST ) {
527648 if ( this . identity && node . rawPrelude ) {
528- return (
649+ this . indent ( 1 ) ;
650+ const result =
529651 this . emit ( node . rawPrelude , node . position ) +
530652 this . emit ( '{' ) +
531- this . mapVisit ( node . declarations ) +
532- this . emit ( '}' )
533- ) ;
653+ this . identityVisitBlock ( node . declarations , 'decls' ) +
654+ this . emit ( '}' ) ;
655+ this . indent ( - 1 ) ;
656+ return result ;
534657 }
535658 const decls = this . stripWhitespace ( node . declarations ) ;
536659 if ( this . compress ) {
@@ -560,12 +683,14 @@ class Compiler {
560683 */
561684 pageMarginBox ( node : CssPageMarginBoxAST ) {
562685 if ( this . identity && node . rawPrelude ) {
563- return (
686+ this . indent ( 1 ) ;
687+ const result =
564688 this . emit ( node . rawPrelude , node . position ) +
565689 this . emit ( '{' ) +
566- this . mapVisit ( node . declarations ) +
567- this . emit ( '}' )
568- ) ;
690+ this . identityVisitBlock ( node . declarations , 'decls' ) +
691+ this . emit ( '}' ) ;
692+ this . indent ( - 1 ) ;
693+ return result ;
569694 }
570695 const decls = this . stripWhitespace ( node . declarations ) ;
571696 if ( this . compress ) {
@@ -603,12 +728,14 @@ class Compiler {
603728 */
604729 host ( node : CssHostAST ) {
605730 if ( this . identity && node . rawPrelude ) {
606- return (
731+ this . indent ( 1 ) ;
732+ const result =
607733 this . emit ( node . rawPrelude , node . position ) +
608734 this . emit ( '{' ) +
609- this . mapVisit ( this . filterEmptyRules ( node . rules ) ) +
610- this . emit ( '}' )
611- ) ;
735+ this . identityVisitBlock ( this . filterEmptyRules ( node . rules ) , 'rules' ) +
736+ this . emit ( '}' ) ;
737+ this . indent ( - 1 ) ;
738+ return result ;
612739 }
613740 const rules = this . stripWhitespace ( node . rules ) ;
614741 if ( this . compress ) {
@@ -681,12 +808,14 @@ class Compiler {
681808 */
682809 scope ( node : CssScopeAST ) {
683810 if ( this . identity && node . rawPrelude ) {
684- return (
811+ this . indent ( 1 ) ;
812+ const result =
685813 this . emit ( node . rawPrelude , node . position ) +
686814 this . emit ( '{' ) +
687- this . mapVisit ( this . filterEmptyRules ( node . rules ) ) +
688- this . emit ( '}' )
689- ) ;
815+ this . identityVisitBlock ( this . filterEmptyRules ( node . rules ) , 'rules' ) +
816+ this . emit ( '}' ) ;
817+ this . indent ( - 1 ) ;
818+ return result ;
690819 }
691820 const prelude = node . scope ? ` ${ node . scope } ` : '' ;
692821 return this . rulesBlock (
@@ -725,12 +854,17 @@ class Compiler {
725854 */
726855 genericAtRule ( node : CssGenericAtRuleAST ) {
727856 if ( this . identity && node . rawPrelude && node . rules ) {
728- return (
857+ this . indent ( 1 ) ;
858+ const result =
729859 this . emit ( node . rawPrelude , node . position ) +
730860 this . emit ( '{' ) +
731- this . mapVisit ( this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ) +
732- this . emit ( '}' )
733- ) ;
861+ this . identityVisitBlock (
862+ this . filterEmptyRules ( < CssAllNodesAST [ ] > node . rules ) ,
863+ 'rules' ,
864+ ) +
865+ this . emit ( '}' ) ;
866+ this . indent ( - 1 ) ;
867+ return result ;
734868 }
735869 const prelude = node . prelude ? ` ${ node . prelude } ` : '' ;
736870 const rules = node . rules
@@ -772,12 +906,14 @@ class Compiler {
772906 const decls = node . declarations ;
773907
774908 if ( this . identity && node . rawPrelude ) {
775- return (
909+ this . indent ( 1 ) ;
910+ const result =
776911 this . emit ( node . rawPrelude , node . position ) +
777912 this . emit ( '{' ) +
778- this . mapVisit ( decls ) +
779- this . emit ( '}' )
780- ) ;
913+ this . identityVisitBlock ( decls , 'decls' ) +
914+ this . emit ( '}' ) ;
915+ this . indent ( - 1 ) ;
916+ return result ;
781917 }
782918
783919 const stripped = this . stripWhitespace ( decls ) ;
0 commit comments