Skip to content

Commit ce49f21

Browse files
committed
feat: juice ignore comments
1 parent a0ed51e commit ce49f21

3 files changed

Lines changed: 396 additions & 8 deletions

File tree

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,88 @@ The `data-embed` attribute will be removed from the output HTML, but no inlining
225225
</style>
226226
```
227227

228+
### Ignoring CSS with comments
229+
230+
You can use special CSS comments to prevent Juice from inlining entire CSS files, rules, or even just declarations.
231+
232+
#### Ignore entire file
233+
234+
Add a `/* juice ignore */` comment on the first line in your CSS:
235+
236+
```html
237+
<style>
238+
/* juice ignore */
239+
body { color: red; }
240+
.test { color: blue; }
241+
</style>
242+
243+
<body>
244+
<div class="test">Hello World</div>
245+
</body>
246+
```
247+
248+
The entire CSS will be ignored (as in, not inlined).
249+
250+
With `removeStyleTags: true`, the whole `<style>` tag will be preserved.
251+
252+
#### Ignore next rule
253+
254+
Add `/* juice ignore next */` before any CSS rule to skip inlining that rule:
255+
256+
```css
257+
body { color: black; }
258+
259+
/* juice ignore next */
260+
h1 {
261+
color: blue;
262+
}
263+
264+
p { color: green; }
265+
```
266+
267+
The `h1` rule will not be inlined, but will be preserved when `removeStyleTags: true`. The `body` and `p` rules will be inlined normally.
268+
269+
#### Ignore next declaration
270+
271+
Add `/* juice ignore next */` inside a CSS rule to skip inlining the next declaration:
272+
273+
```css
274+
.test {
275+
color: red;
276+
/* juice ignore next */
277+
font-weight: bold;
278+
font-size: 14px;
279+
}
280+
```
281+
282+
The `color` and `font-size` will be inlined, but `font-weight` will not. The rule with the ignored declaration will be preserved in a `<style>` tag when `removeStyleTags: true`.
283+
284+
#### Ignore blocks of code
285+
286+
Use start/end comment pairs to ignore multiple rules:
287+
288+
```css
289+
h1 {
290+
color: black;
291+
}
292+
293+
/* juice start ignore */
294+
h2 {
295+
color: pink;
296+
}
297+
298+
h3 {
299+
color: lightcoral;
300+
}
301+
/* juice end ignore */
302+
303+
h4 {
304+
color: green;
305+
}
306+
```
307+
308+
The `h2` and `h3` rules will not be inlined but will be preserved in a `<style>` tag when `removeStyleTags: true`. The `h1` and `h4` rules will be inlined normally.
309+
228310
### CLI Options
229311

230312
To use Juice from CLI, run `juice [options] input.html output.html`

lib/utils.js

Lines changed: 164 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,81 @@ exports.parseCSS = function(css) {
6060
var parsed = mensch.parse(css, {position: true, comments: true});
6161
var rules = typeof parsed.stylesheet != 'undefined' && parsed.stylesheet.rules ? parsed.stylesheet.rules : [];
6262
var ret = [];
63+
var ignoring = false;
64+
var ignoreEntireFile = false;
65+
66+
// Check if the first rule is a comment that ignores the entire file
67+
if (rules.length > 0 && rules[0].type === 'comment' && rules[0].text) {
68+
var firstComment = rules[0].text.trim();
69+
if (firstComment === 'juice ignore') {
70+
ignoreEntireFile = true;
71+
}
72+
}
73+
74+
if (ignoreEntireFile) {
75+
return ret;
76+
}
6377

6478
for (var i = 0, l = rules.length; i < l; i++) {
79+
// Handle comments for ignore directives
80+
if (rules[i].type === 'comment') {
81+
if (!rules[i].text) {
82+
continue;
83+
}
84+
var comment = rules[i].text.trim();
85+
86+
if (comment === 'juice start ignore') {
87+
ignoring = true;
88+
} else if (comment === 'juice end ignore') {
89+
ignoring = false;
90+
} else if (comment === 'juice ignore next') {
91+
// Skip the next rule
92+
i++;
93+
}
94+
continue;
95+
}
96+
97+
// Skip rules if we're in an ignore block
98+
if (ignoring) {
99+
continue;
100+
}
101+
65102
if (rules[i].type == 'rule') {
66103
var rule = rules[i];
67104
var selectors = rule.selectors;
68105

69-
for (var ii = 0, ll = selectors.length; ii < ll; ii++) {
70-
ret.push([selectors[ii], rule.declarations]);
106+
// Check if this rule has 'juice ignore next' in its declarations
107+
// If so, we need to skip specific declarations that follow the comment
108+
var filteredDeclarations = [];
109+
if (rule.declarations) {
110+
var skipNext = false;
111+
for (var d = 0; d < rule.declarations.length; d++) {
112+
var decl = rule.declarations[d];
113+
114+
if (decl.type === 'comment' && decl.text) {
115+
var declComment = decl.text.trim();
116+
if (declComment === 'juice ignore next') {
117+
skipNext = true;
118+
continue;
119+
}
120+
}
121+
122+
if (skipNext && decl.type === 'property') {
123+
skipNext = false;
124+
continue;
125+
}
126+
127+
if (decl.type === 'property') {
128+
filteredDeclarations.push(decl);
129+
}
130+
}
131+
}
132+
133+
// Only add rule if it has declarations after filtering
134+
if (filteredDeclarations.length > 0) {
135+
for (var ii = 0, ll = selectors.length; ii < ll; ii++) {
136+
ret.push([selectors[ii], filteredDeclarations]);
137+
}
71138
}
72139
}
73140
}
@@ -88,20 +155,109 @@ exports.getPreservedText = function(css, options, ignoredPseudos) {
88155
var rules = typeof parsed.stylesheet != 'undefined' && parsed.stylesheet.rules ? parsed.stylesheet.rules : [];
89156
var preserved = [];
90157
var lastStart = null;
158+
var ignoring = false;
159+
var ignoreBlock = [];
160+
var ignoreEntireFile = false;
161+
var ignoredRuleIndices = new Set();
162+
163+
// Check if the first rule is a comment that ignores the entire file
164+
if (rules.length > 0 && rules[0].type === 'comment' && rules[0].text) {
165+
var firstComment = rules[0].text.trim();
166+
if (firstComment === 'juice ignore') {
167+
ignoreEntireFile = true;
168+
}
169+
}
170+
171+
// If ignoring entire file, preserve everything
172+
if (ignoreEntireFile) {
173+
return '\n' + css + '\n';
174+
}
175+
176+
// First pass: identify rules to ignore/preserve (forward iteration for "ignore next")
177+
for (var i = 0; i < rules.length; i++) {
178+
if (rules[i].type === 'comment' && rules[i].text) {
179+
var comment = rules[i].text.trim();
180+
if (comment === 'juice ignore next' && i + 1 < rules.length) {
181+
ignoredRuleIndices.add(i + 1);
182+
}
183+
}
91184

185+
// Also check for declarations with "juice ignore next"
186+
if (rules[i].type === 'rule' && rules[i].declarations) {
187+
for (var d = 0; d < rules[i].declarations.length; d++) {
188+
if (rules[i].declarations[d].type === 'comment' && rules[i].declarations[d].text) {
189+
var declComment = rules[i].declarations[d].text.trim();
190+
if (declComment === 'juice ignore next') {
191+
ignoredRuleIndices.add(i);
192+
break;
193+
}
194+
}
195+
}
196+
}
197+
}
198+
199+
// Second pass: process rules (backward iteration for proper order)
92200
for (var i = rules.length - 1; i >= 0; i--) {
93-
if ((options.fontFaces && rules[i].type === 'font-face') ||
94-
(options.mediaQueries && rules[i].type === 'media') ||
95-
(options.keyFrames && rules[i].type === 'keyframes') ||
96-
(options.pseudos && rules[i].selectors && this.matchesPseudo(rules[i].selectors[0], ignoredPseudos))) {
201+
var rule = rules[i];
202+
203+
// Handle juice ignore comments
204+
if (rule.type === 'comment') {
205+
if (!rule.text) {
206+
continue;
207+
}
208+
var comment = rule.text.trim();
209+
210+
if (comment === 'juice end ignore') {
211+
ignoring = true;
212+
ignoreBlock.push(rule);
213+
} else if (comment === 'juice start ignore') {
214+
ignoring = false;
215+
ignoreBlock.push(rule);
216+
// Stringify the entire ignore block
217+
if (ignoreBlock.length > 0) {
218+
preserved.unshift(
219+
mensch.stringify(
220+
{ stylesheet: { rules: ignoreBlock.reverse() } },
221+
{ comments: true, indentation: ' ' }
222+
)
223+
);
224+
ignoreBlock = [];
225+
}
226+
}
227+
// Skip "juice ignore next" comments themselves
228+
continue;
229+
}
230+
231+
// If we're in an ignore block, collect the rules
232+
if (ignoring) {
233+
ignoreBlock.push(rule);
234+
continue;
235+
}
236+
237+
// Check if this rule was marked to be ignored
238+
if (ignoredRuleIndices.has(i)) {
239+
preserved.unshift(
240+
mensch.stringify(
241+
{ stylesheet: { rules: [rule] } },
242+
{ comments: true, indentation: ' ' }
243+
)
244+
);
245+
continue;
246+
}
247+
248+
// Original preserve logic
249+
if ((options.fontFaces && rule.type === 'font-face') ||
250+
(options.mediaQueries && rule.type === 'media') ||
251+
(options.keyFrames && rule.type === 'keyframes') ||
252+
(options.pseudos && rule.selectors && this.matchesPseudo(rule.selectors[0], ignoredPseudos))) {
97253
preserved.unshift(
98254
mensch.stringify(
99-
{ stylesheet: { rules: [ rules[i] ] }},
255+
{ stylesheet: { rules: [rule] } },
100256
{ comments: false, indentation: ' ' }
101257
)
102258
);
103259
}
104-
lastStart = rules[i].position.start;
260+
lastStart = rule.position.start;
105261
}
106262

107263
if (preserved.length === 0) {

0 commit comments

Comments
 (0)