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

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mr.baileys’s picture

Status: Needs review » Closed (won't fix)
Issue tags: +CSS aggregation

according to the CSS specs:

5.2.1 Grouping
When several selectors share the same declarations, they may be grouped into a comma-separated list.

So separating the selectors by just a newline is invalid CSS IMO, so I don't think we should fix this...

rfay’s picture

Status: Closed (won't fix) » Needs review

This 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

<div class="this"> <div class="is"><div class="an"><div class="error">This DIV only would be matched</div></div></div></div>
mr.baileys’s picture

I apologize, the indentation used in the example made me think of multiple selectors. You're right, this does look like an error.

catch’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Quick fix

Very simple patch, explanation is good, marking rtbc.

webchick’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Hm. 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.

rfay’s picture

Status: Needs work » Needs review
FileSize
79.8 KB

Here 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.

catch’s picture

Status: Needs review » Needs work

We 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.

Heine’s picture

From http://www.w3.org/TR/CSS2/syndata.html#whitespace

The token S in the grammar above stands for white space. Only the characters "space" (U+0020), "tab" (U+0009), "line feed" (U+000A), "carriage return" (U+000D), and "form feed" (U+000C) can occur in white space. Other space-like characters, such as "em-space" (U+2003) and "ideographic space" (U+3000), are never part of white space.

The patch above is unlikely to handle "form feed" or 0x0c properly.

rfay’s picture

@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.

rfay’s picture

Status: Needs work » Needs review
FileSize
654 bytes

Per @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.

rfay’s picture

After discussion with webchick, I've rerolled this

  • It's one patch, the one-liner to fix the bug, and a test suite to test drupal_load_stylesheet_content()
  • Per feedback, the input CSS file is a very simple file
  • Coding standards issues have been addressed

Status: Needs review » Needs work

The last submitted patch failed testing.

webchick’s picture

Status: Needs work » Needs review

Submitting for re-testing.

rfay’s picture

Issue tags: -Needs tests

Now has tests, so I removed the "Needs test" tag.

RobLoach’s picture

Priority: Normal » Critical
Status: Needs review » Needs work

Pushing 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:

.field .field-label,.field .field-label-inline,.field .field-label-inline-first{font-weight:bold;}.node-unpublished{background-color:#fff4f4;}.preview .node{background-color:#ffffea;}

After patch:

.field .field-label,.field .field-label-inline,.field .field-label-inline-first{font-weight:bold;}

.node-unpublished{background-color:#fff4f4;}.preview .node{background-color:#ffffea;}

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?

rfay’s picture

Status: Needs work » Needs review

@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.

RobLoach’s picture

Status: Needs review » Reviewed & tested by the community

That 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!

RobLoach’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
8.16 KB

My nitpicking side got the better of me.

Status: Needs review » Needs work

The last submitted patch failed testing.

rfay’s picture

Status: Needs work » Needs review
FileSize
8.55 KB

OK, 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:

  • Per RobLoach's concerns, the css test files are now .css files
  • The test in common.test is now in a DrupalUnitTestCase instead of a DrupalWebTestCase. Since this is a very basic unit test with no state and no web browser simulation required, this is just a little easier on the test servers.

Hoping we can put this to bed. It's just one line of code!

RobLoach’s picture

Status: Needs review » Reviewed & tested by the community

Wheee!

webchick’s picture

Status: Reviewed & tested by the community » Needs work

Wow, 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?

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.

We should maybe have a reference off to said spec in the PHPDoc comments of drupal_load_stylesheet_content() because atm output like:

...color:#494949;}.this
+.is
+.a
+.test{font:1em/100%...

...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.

---

+++ includes/common.inc	17 Jul 2009 05:43:44 -0000
@@ -2654,8 +2654,7 @@
       \s*([@{}:;,]|\)\s|\s\()\s* |  # Remove whitespace around separators, but keep space around parentheses.
-      /\*([^*\\\\]|\*(?!/))+\*/ |   # Remove comments that are not CSS hacks.
-      [\n\r]                        # Remove line breaks.
+      /\*([^*\\\\]|\*(?!/))+\*/    # Remove comments that are not CSS hacks.

Looks like that # comment needs to be indented to be inline with the others.

+++ modules/simpletest/tests/common.test	17 Jul 2009 05:43:45 -0000
@@ -282,6 +282,47 @@
+ * CSS Unit Tests

Comments need to end in a period.

+++ modules/simpletest/tests/common.test	17 Jul 2009 05:43:45 -0000
@@ -282,6 +282,47 @@
+      'name' => 'CSS Unit Testss',

"Tests" ;)

+++ modules/simpletest/tests/common.test	17 Jul 2009 05:43:45 -0000
@@ -282,6 +282,47 @@
+    /**
+   * Tests basic CSS loading with and without optimization (drupal_load_stylesheet())
+   */

Indentation is off here. Must end with a period as well.

+++ modules/simpletest/tests/common.test	17 Jul 2009 05:43:45 -0000
@@ -282,6 +282,47 @@
+  // Array of files to test. Original = .css,
+  // unoptimized expected output = .css.unoptimized.css,
+  //   optimized expected = .css.optimized.css
+  // They live in the simpletest/files/css_test_files directory

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.

+++ modules/simpletest/tests/common.test	17 Jul 2009 05:43:45 -0000
@@ -282,6 +282,47 @@
+      $unoptimized_output = drupal_load_stylesheet_content($content, FALSE);
+      
+      $expected = file_get_contents("$path/$file.unoptimized.css");

Trailing whitespace in the middle there.

+++ modules/simpletest/files/css_test_files/css_input_with_import.css	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,34 @@
+/**
+  * @file Basic css that uses @import
+*/

Indentation off here.

+++ modules/simpletest/files/css_test_files/css_input_with_import.css	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,34 @@
+body {
+  margin: 0;
+  padding: 0;
+  background: #edf5fa;
+  font: 76%/170% Verdana, sans-serif;
+  color: #494949;
+}
...
+textarea, select {
+  font: 1em/160% Verdana, sans-serif;
+  color: #494949;
+}

Is this test data relevant at all? If not, let's remove it. Keep the test data as simple as possible.

+++ modules/simpletest/files/css_test_files/css_input_without_import.css.unoptimized.css	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,6 @@
+/* $Id: style.css,v 1.57 2009/05/31 00:51:41 webchick Exp $ */
+
+/**
+  * @file Basic css that does not use 
+}
+

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.

+++ modules/simpletest/files/css_test_files/css_input_without_import.css	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,33 @@
+/**
+  * @file Basic css that does not use @import
+*/

Indentation.

+++ modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,34 @@
+/**
+  * @file Basic css that uses @import
+*/
+
+
+
+

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.

rfay’s picture

Status: Needs work » Needs review
FileSize
8.32 KB

@webchick, thanks for the detailed review! You are a stickler!

First, you asked me to explain:

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.

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 :-)

Status: Needs review » Needs work

The last submitted patch failed testing.

sun.core’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch failed testing.

rfay’s picture

Status: Needs work » Needs review
FileSize
5.22 KB

OK, 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.

RobLoach’s picture

FileSize
5.11 KB

Found a couple other small indentation problems. Other then that it looks really good. Here's the updated patch.

catch’s picture

Status: Needs review » Reviewed & tested by the community

This 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.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Great, 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.

webchick’s picture

Version: 7.x-dev » 6.x-dev
Status: Fixed » Patch (to be ported)

Doing what I said I was doing. :P

catch’s picture

Version: 6.x-dev » 7.x-dev
Status: Patch (to be ported) » Active

The test bot doesn't like this now it's committed - 1 fail being reported from 'CSS unit tests' for every new test result.

catch’s picture

I can reproduce locally.

mfb’s picture

The first lines of the files do not match:

< /* $Id: css_input_without_import.css,v 1.1 2009/10/05 02:48:38 webchick Exp $ */
---
> /* $Id: css_input_without_import.css.unoptimized.css,v 1.1 2009/10/05 02:48:38 webchick Exp $ */
mfb’s picture

Status: Active » Needs review
FileSize
1.14 KB

Something like this will get the test passing.

catch’s picture

Status: Needs review » Reviewed & tested by the community

That 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.

sun’s picture

Almost. The file name was changed in one line but not the other.

However, that test needs an overhaul anyway. So take this.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Wow, 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.

RobLoach’s picture

As an added bonus, Sun switched it to a DrupalUnitTestCase, which will improve testing performance! YAY!

andypost’s picture

Version: 7.x-dev » 6.x-dev
Status: Fixed » Needs review
FileSize
846 bytes

Backport to D6

rfay’s picture

Status: Needs review » Reviewed & tested by the community

This is the same one we already hashed through for D7, and we should get it into D6.

RobLoach’s picture

Still yup.

Gábor Hojtsy’s picture

Status: Reviewed & tested by the community » Fixed

Committed to Drupal 6 too, thanks.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

Coornail’s picture

I'm not sure if I supposed to reopen this ticket or open a new one or just leave it but:

  • We could remove the complately blank lines (/^$/ or /(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/)
  • We could remove the trailing and beginning spaces and tabs (trim()?)

I'll show you an example:

Before:

[empty lines]
[empty lines]
.node-unpublished{background-color:#fff4f4;}.preview .node{background-color:#ffffea;}#node-admin-filter ul{list-style-type:none;padding:0;margin:0;width:100%;}#node-admin-buttons{float:left;
  margin-left:0.5em;
  clear:right;}td.revision-current{background:#ffc;}.node-form .form-text{display:block;width:95%;}.node-form .container-inline .form-text{display:inline;width:auto;}.node-form .standard{clear:both;}.node-form textarea{display:block;width:95%;}.node-form .attachments fieldset{float:none;display:block;}.terms-inline{display:inline;}

After:

.node-unpublished{background-color:#fff4f4;}.preview .node{background-color:#ffffea;}#node-admin-filter ul{list-style-type:none;padding:0;margin:0;width:100%;}#node-admin-buttons{float:left;
margin-left:0.5em;
clear:right;}td.revision-current{background:#ffc;}.node-form .form-text{display:block;width:95%;}.node-form .container-inline .form-text{display:inline;width:auto;}.node-form .standard{clear:both;}.node-form textarea{display:block;width:95%;}.node-form .attachments fieldset{float:none;display:block;}.terms-inline{display:inline;}
Dropfish’s picture

Version: 6.x-dev » 6.16
Priority: Critical » Normal
Status: Closed (fixed) » Active

Hi, 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". :-/

andypost’s picture

Version: 6.16 » 6.x-dev
Priority: Normal » Critical
Status: Active » Closed (fixed)