@@ -34,12 +34,16 @@ import {
3434export type CompilerOptions = {
3535 indent ?: string ;
3636 compress ?: boolean ;
37+ identity ?: boolean ;
38+ removeEmptyRules ?: boolean ;
3739} ;
3840
3941class Compiler {
4042 level = 0 ;
4143 indentation = ' ' ;
4244 compress = false ;
45+ identity = false ;
46+ removeEmptyRules = false ;
4347
4448 constructor ( options ?: CompilerOptions ) {
4549 if ( typeof options ?. indent === 'string' ) {
@@ -48,6 +52,12 @@ class Compiler {
4852 if ( options ?. compress ) {
4953 this . compress = true ;
5054 }
55+ if ( options ?. identity ) {
56+ this . identity = true ;
57+ }
58+ if ( options ?. removeEmptyRules ) {
59+ this . removeEmptyRules = true ;
60+ }
5161 }
5262
5363 // We disable no-unused-vars for _position. We keep position for potential reintroduction of source-map
@@ -154,18 +164,19 @@ class Compiler {
154164 rules : Array < CssAllNodesAST > ,
155165 position ?: CssCommonPositionAST [ 'position' ] ,
156166 ) {
167+ const filteredRules = this . filterEmptyRules ( rules ) ;
157168 if ( this . compress ) {
158169 return (
159170 this . emit ( header , position ) +
160171 this . emit ( '{' ) +
161- this . mapVisit ( rules ) +
172+ this . mapVisit ( filteredRules ) +
162173 this . emit ( '}' )
163174 ) ;
164175 }
165176 return (
166177 this . emit ( `${ this . indent ( ) } ${ header } ` , position ) +
167178 this . emit ( ` {\n${ this . indent ( 1 ) } ` ) +
168- this . mapVisit ( rules , '\n\n' ) +
179+ this . mapVisit ( filteredRules , '\n\n' ) +
169180 this . emit ( `\n${ this . indent ( - 1 ) } ${ this . indent ( ) } }` )
170181 ) ;
171182 }
@@ -197,18 +208,81 @@ class Compiler {
197208 }
198209
199210 compile ( node : CssStylesheetAST ) {
211+ if ( this . identity ) {
212+ return this . identityCompile ( node ) ;
213+ }
200214 if ( this . compress ) {
201- return node . stylesheet . rules . map ( this . visit , this ) . join ( '' ) ;
215+ return this . filterEmptyRules ( node . stylesheet . rules )
216+ . map ( this . visit , this )
217+ . join ( '' ) ;
202218 }
203219
204220 return this . stylesheet ( node ) ;
205221 }
206222
223+ /**
224+ * Identity mode: reconstruct the original CSS using stored source offsets.
225+ * Falls back to beautified output when offsets are not available.
226+ */
227+ private identityCompile ( node : CssStylesheetAST ) : string {
228+ const source = node . stylesheet . originalSource ;
229+ if ( ! source ) {
230+ return this . stylesheet ( node ) ;
231+ }
232+
233+ const allRules = node . stylesheet . rules ;
234+ if ( allRules . length === 0 ) {
235+ return source ;
236+ }
237+
238+ // Collect all nodes with valid offsets
239+ const nodesWithOffsets : Array < {
240+ startOffset : number ;
241+ endOffset : number ;
242+ } > = [ ] ;
243+ for ( const rule of allRules ) {
244+ const pos = ( rule as CssCommonPositionAST ) . position ;
245+ if ( pos ?. start ?. offset != null && pos ?. end ?. offset != null ) {
246+ nodesWithOffsets . push ( {
247+ startOffset : pos . start . offset ,
248+ endOffset : pos . end . offset ,
249+ } ) ;
250+ }
251+ }
252+
253+ if ( nodesWithOffsets . length === 0 ) {
254+ return this . stylesheet ( node ) ;
255+ }
256+
257+ // Reconstruct: output everything from start to end of last node,
258+ // then any trailing text.
259+ const lastEnd = nodesWithOffsets [ nodesWithOffsets . length - 1 ] . endOffset ;
260+ return source . slice ( 0 , lastEnd ) + source . slice ( lastEnd ) ;
261+ }
262+
207263 /**
208264 * Visit stylesheet node.
209265 */
210266 stylesheet ( node : CssStylesheetAST ) {
211- return this . mapVisit ( node . stylesheet . rules , '\n\n' ) ;
267+ return this . mapVisit ( this . filterEmptyRules ( node . stylesheet . rules ) , '\n\n' ) ;
268+ }
269+
270+ /**
271+ * Filter out empty rules when removeEmptyRules is enabled.
272+ */
273+ private filterEmptyRules < T extends CssAllNodesAST > (
274+ rules : Array < T > ,
275+ ) : Array < T > {
276+ if ( ! this . removeEmptyRules ) {
277+ return rules ;
278+ }
279+ return rules . filter ( ( rule ) => {
280+ if ( rule . type === CssTypes . rule ) {
281+ const r = rule as unknown as CssRuleAST ;
282+ return r . declarations . length > 0 ;
283+ }
284+ return true ;
285+ } ) ;
212286 }
213287
214288 /**
@@ -562,6 +636,9 @@ class Compiler {
562636 const decls = node . declarations ;
563637
564638 if ( this . compress ) {
639+ if ( this . removeEmptyRules && ! decls . length ) {
640+ return '' ;
641+ }
565642 return (
566643 this . emit ( node . selectors . join ( ',' ) , node . position ) +
567644 this . emit ( '{' ) +
@@ -572,6 +649,9 @@ class Compiler {
572649 const indent = this . indent ( ) ;
573650
574651 if ( ! decls . length ) {
652+ if ( this . removeEmptyRules ) {
653+ return '' ;
654+ }
575655 return (
576656 this . emit (
577657 node . selectors
0 commit comments