Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
drupal_load_stylesheet_content(), in common.inc line 2574, is in charge of aggregating and compressing CSS. It attempts to remove whitespace, but goes too far.
The CSS fragment:
div.this
div.is
div.an
div.error { display: inline; }
is compressed completely inappropriately into
div.thisdiv.isdiv.andiv.error{display:inline;}
where it should have been
div.this div.is div.an div.error {display:inline;}
The attached patch attempts to solve this by first replacing newlines with spaces. After that the whitespace can be compressed.
This bug also exists in Drupal 6.x
Comment | File | Size | Author |
---|---|---|---|
#40 | 472820_css_optimization_bug.patch | 846 bytes | andypost |
#37 | drupal.css-load-unit-test.patch | 3.04 KB | sun |
#35 | css.patch | 1.14 KB | mfb |
#28 | 472820.patch | 5.11 KB | RobLoach |
#27 | css_optimization_bug_06.patch | 5.22 KB | rfay |
Comments
Comment #1
mr.baileysaccording to the CSS specs:
So separating the selectors by just a newline is invalid CSS IMO, so I don't think we should fix this...
Comment #2
rfayThis example is not proper for a comma-separated list (sharing the same specification). This is three selectors declaring specificity - only the innermost matches.
div.this div.is div.an div.error
It would match only
Comment #3
mr.baileysI apologize, the indentation used in the example made me think of multiple selectors. You're right, this does look like an error.
Comment #4
catchVery simple patch, explanation is good, marking rtbc.
Comment #5
webchickHm. I'd feel a lot more comfortable committing this if we had tests to prove that we don't break something else in the meantime.
Comment #6
rfayHere is a test for that section of the code. It's a pretty simple approach, but may be adequate for now.
Essentially, this test just takes and loads a set of CSS stylesheets and then compares the result (optimized or not) against a static set of expected results.
The function drupal_load_stylesheet_content() does two basic things: Importing @imports, and compressing the whitespace out of the results (of $optimize is set). So this test attempts to deal with that.
The bad thing that I don't know how to get past with this approach: This bug is about inappropriate removal of linefeeds (which are valid whitespace). So I haven't figured out how to write a test which will work *before* the patch and then also work *after* it, since the patch deliberately changes the output (by replacing linefeeds with spaces instead of removing them). So I'm open to suggestions on this.
Can anyone suggest an approach that will test the old (broken) code as valid and then still test the new (patched) code as valid? I know that one of the objectives of this exercise is to make sure we're not breaking anything existing. On the other hand, writing a test to validate broken code is wrong.
Comment #7
catchWe should be able to use a small core CSS file for testing - no need to duplicate the whole of Garland's here. Not sure what you mean by testing the old broken code as valid - ideally the test should fail without your patch, and pass with it.
Comment #8
Heine CreditAttribution: Heine commentedFrom http://www.w3.org/TR/CSS2/syndata.html#whitespace
The patch above is unlikely to handle "form feed" or 0x0c properly.
Comment #9
rfay@catch: the CSS input file could definitely be simpler. However, I'm attempting to catch a variety of problems using a very simple technique. Using a more significant and diverse CSS file is more likely to show unintended change. Since I'm not attempting real validation (which would require parsing both files and comparing them semantically), this technique at least tries to get results on a more significant level.
To reiterate the problem with creating an appropriate test: With this simple test technique, this test only validates that the code behaves the same as it used to (static input producing the same static output). Thus, when you change how the code behaves (by handling linefeeds as whitespace instead of deleting them), the test will fail after the patch is applied. Since I think webchick's idea was to create a test which would validate that the patch was not breaking anything new, this won't accomplish that goal. Because the test will break when the patch is applied.
Comment #10
rfayPer @heine's comment I revisited the patch and re-studied the preg expression.
Since \s does cover formfeed, newlines, tabs, and spaces, the patch can be expressed more simply and with fewer changes and probably no side-effects at all.
The attached re-rolled patch does nothing except to delete the portion of the regex that was removing \r\n. Since they're legitimate whitespace, that's the error in this; they can't be removed. They *could* be optimized further, but that should probably be addressed separately.
Comment #11
rfayAfter discussion with webchick, I've rerolled this
Comment #13
webchickSubmitting for re-testing.
Comment #14
rfayNow has tests, so I removed the "Needs test" tag.
Comment #15
RobLoachPushing to critical as this can break some CSS files.
Thank you for noticing this. I like what you're doing here but the optimized CSS files now have a bunch of line breaks that they don't need to have. Before patch:
After patch:
As you can see, we have two line breaks here that we don't need to have. Is there anyway we could remove the line breaks, but add an extra space instead for the use case you provided so that the CSS still stays valid?
Comment #16
rfay@Rob Loach: Thanks for testing.
The reality is that linefeeds are valid whitespace in CSS, so removing them breaks the spec. In an earlier version of the patch, I replaced them with spaces, but decided later that that added risk to the patch.
Since we're only doing a minimal big of CSS compression here, without parsing the CSS, it's my opinion that the linefeeds, as valid CSS, must be treated with respect and not removed.
Comment #17
RobLoachThat sounds reasonable. This is aggregation, not compression. It seems like if we want to compress it further, we'd want a pluggable framework for the CSS. Setting it to RTBC!
Comment #18
RobLoachMy nitpicking side got the better of me.
Comment #20
rfayOK, one? more time?
This patch has the same fix, no change, to the bug at hand. It has not changed since #11.
What has changed:
Hoping we can put this to bed. It's just one line of code!
Comment #21
RobLoachWheee!
Comment #22
webchickWow, sorry. It's apparently taken me a very long time to get back to this issue! :(
I've reviewed this and mostly found minor formatting stuff, and a couple of things that look like bugs.
@rfay, could you explain this?
We should maybe have a reference off to said spec in the PHPDoc comments of drupal_load_stylesheet_content() because atm output like:
...looks like a bug in our optimizer.
Here's the review. Note that since this doesn't change APIs, I probably won't get back to it again before code freeze. But it's marked as a critical bug so it will get fixed before D7 ships.
---
Looks like that # comment needs to be indented to be inline with the others.
Comments need to end in a period.
"Tests" ;)
Indentation is off here. Must end with a period as well.
Indentation/spacing is off here. These comments should be indented to the same width as the body of the function.
That said, these are probably slightly too detailed comments (it's spending a lot of time explaining stuff that's easily deduceable by reading the code). I'd go with something like:
// Test with two sample CSS files; one using the @import tag, and one without.
Trailing whitespace in the middle there.
Indentation off here.
Is this test data relevant at all? If not, let's remove it. Keep the test data as simple as possible.
Is this file intentionally all messed up? I can't see how that could possibly be valid CSS.
If it is intentional, please add more comments here so someone doesn't try and "fix" it later.
Indentation.
Indentation.
Also, what's up with all of those blank lines? Part of the test case? If so, maybe document that in the @file comment? I could see someone "fixing" this later.
23 days to code freeze. Better review yourself.
Comment #23
rfay@webchick, thanks for the detailed review! You are a stickler!
First, you asked me to explain:
That's the essence of what this patch is about. The old code assumed that any linefeeds or carriage returns could just be gratuitously removed. In reality, they are whitespace per the spec (http://www.w3.org/TR/CSS2/syndata.html#whitespace), so a return between two parts of a CSS selector is as valid as a space. That's why the classic example is
.this
.is
.an
.example { display: none; }
which in CSS terms is completely equivalent to
.this .is .an .example { display: none; }
and is the type of case that the old code broke. (It "compressed" the code into .this.is.an.example { display: none })
Second, I believe that the formatting issues you pointed out are resolved in the attached patch.
Third, I want to mention that the test developed here is intended to be a generic and extensible test of CSS aggregation. It's not just a test for this bug, although I did try to slip that in. That's the reason for some of the extra comments, for example. It's also the reason there's some generic CSS in the test files - it's not just about this bug.
Fourth, the truncated test css file that you noticed was another bug. I've never been much of a user of @import, so I just assumed everything was fine. But our CSS aggregator unfortunately eats "@import" wherever it appears. Since it was in the comment at the top of the file, and didn't have the file argument, our aggregator just ate the rest of the file. When I sorted this out thanks to your keen eye, I posted #544568: CSS aggregation attempts to process @import in comment about it. It's relatively minor, but more of an indication that our CSS techniques in general are pretty crude.
In this case I took the (I think) fairly reasonable step of removing the word "@import" from the css comment, #544568: CSS aggregation attempts to process @import in comment has nothing to do with the issue we're working on here.
Fifth, I did have to make a couple of changes to the test code. It now calls drupal_load_stylesheet() instead of drupal_load_stylesheet_content(), which doesn't handle @import correctly. I switched back to DrupalWebTestCase from DrupalUnitTestCase as the @import didn't work correctly at all in the unit test environment.
Anyway, thanks. This sure is an exhausting one-line fix :-)
Comment #25
sun.core CreditAttribution: sun.core commentedComment #27
rfayOK, here's the patch re-rolled after the commit of #584370: Enable Optimize CSS files on performance page & Drupal dies. I did scale back the test to only test the issue in this bug, rather than trying for more. No change the the actual fix at all, nor has there been forever. Let's get this in and get a few more people out of unpredictable css hell. This needs to go into 6.x too, which is where I originally studied it.
Comment #28
RobLoachFound a couple other small indentation problems. Other then that it looks really good. Here's the updated patch.
Comment #29
catchThis looks great. I keep wanting to remove the extra blank linkes in test.css - but it's a test, so it ought to be realistic.
Comment #30
webchickGreat, thanks for the fixes. Let's maybe tweak our whitespace algorithm in the future so that it strips those because I agree it does look very odd.
Committed to HEAD. Marking needs porting to 6.x, although it's possible one of the other patches has it; I kinda lost track.
Comment #31
webchickDoing what I said I was doing. :P
Comment #32
catchThe test bot doesn't like this now it's committed - 1 fail being reported from 'CSS unit tests' for every new test result.
Comment #33
catchI can reproduce locally.
Comment #34
mfbThe first lines of the files do not match:
Comment #35
mfbSomething like this will get the test passing.
Comment #36
catchThat makes sense, looks a bit odd but I can't think of anything better either, and it's an odd bug in the first place.
Comment #37
sunAlmost. The file name was changed in one line but not the other.
However, that test needs an overhaul anyway. So take this.
Comment #38
webchickWow, that's totally bizarre. I have no idea why this wasn't failing tests while it was in the queue. Sorry, guys. :(
Anyway, committed to HEAD.
Comment #39
RobLoachAs an added bonus, Sun switched it to a DrupalUnitTestCase, which will improve testing performance! YAY!
Comment #40
andypostBackport to D6
Comment #41
rfayThis is the same one we already hashed through for D7, and we should get it into D6.
Comment #42
RobLoachStill yup.
Comment #43
Gábor HojtsyCommitted to Drupal 6 too, thanks.
Comment #45
Coornail CreditAttribution: Coornail commentedI'm not sure if I supposed to reopen this ticket or open a new one or just leave it but:
/^$/
or/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/
)trim()
?)I'll show you an example:
Before:
After:
Comment #46
Dropfish CreditAttribution: Dropfish commentedHi, I'm not sure if re-opening this bug is appropriate...
I have encountered another similar problem with 6.16. For my custom theme, the following CSS:
#foo :visited { }
gets turned into
#foo:visited { }
which is obviously not the same... Quick workaround for me is to use this instead:
#foo *:visited { }
By the way, I would actually prefer to get all CSS aggregated into one file, but *without* munging it at the same time. That's not possible, the current setting is "all or nothing". :-/
Comment #47
andypostUpdates happen in #444228: Optimize CSS option causes php cgi to segfault in pcre function "match"
Please do not reopen this issue