Problem/Motivation

Escaped characters in CSS classes are valid. Several popular CSS/Sass libraries use escaped characters (particularly ones which use the BEM methodology) to denote special uses or characteristics of a CSS class.

For instance, the inuitcss CSS library uses the @ character in CSS classes for identifying responsive breakpoint classes and the forward slash / character for fractional grid column classes.

Example CSS with Escaped Characters

.u-1\/1 {
  // CSS here.
}

.u-1\/2\/@tablet {
  // CSS here.
}

.u-1\/3\/@desktop {
  // CSS here.
}

Example Markup Using CSS classes with escaped characters

<div class="o-layout__item u-1/1 u-1/2@tablet u-1/3@desktop">

The Html::cleanCssIdentifier function strips escaped characters however, making Drupal incompatible with CSS which uses them.

Drupal Example of Problem

In a twig template, using the clean_class filter (which uses Html::cleanCssIdentifier), such as:

{% set classes = [
  'o-layout__item',
  ('u-1/1'|clean_class),
  ('u-1/2@tablet'|clean_class),
  ('u-1/3@desktop'|clean_class),
  ('u-1/3@desktop'|clean_class)
  ]
%}
<div{{ attributes.addClass(classes) }}>I'm a div with grid classes</div>

produces the following markup:

<div class="o-layout__item u-1-1 u-1-2tablet u-1-3desktop">I'm a div with grid classes</div>

when the markup should instead be:

<div class="o-layout__item u-1/1 u-1/2@tablet u-1/3@desktop">I'm a div with grid classes</div>

Proposed resolution

Update the regex used in the current Html::cleanCssIdentifier

    // Valid characters in a CSS identifier are:
    // - the hyphen (U+002D)
    // - a-z (U+0030 - U+0039)
    // - A-Z (U+0041 - U+005A)
    // - the underscore (U+005F)
    // - 0-9 (U+0061 - U+007A)
    // - ISO 10646 characters U+00A1 and higher
    // We strip out any character not in the above list.
    $identifier = preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);
    // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
    $identifier = preg_replace([
    '/^[0-9]/',
    '/^(-[0-9])|^(--)/'
    ], ['_', '__'], $identifier);
    return $identifier;

to allow for escaped characters.

Issue fork drupal-2916377

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

PCate created an issue. See original summary.

pcate’s picture

Issue summary: View changes
dawehner’s picture

@PCate Please provide a patch with ideally test coverage as well. I just had a look and your comment makes totally sense to be in line with the w3c standard: https://www.w3.org/TR/CSS21/syndata.html#characters

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

daniel kulbe’s picture

Version: 8.5.x-dev » 8.6.x-dev
Issue tags: +CSS, +html attribute
daniel kulbe’s picture

I can confirm, this is also an issue for the UIKit framework / theme. Responsive classes are also addressed with @ (U+0040).

So I added a patch, which covers the "@ sign", use by many CSS frameworks in responsive breakpoint selectors, and the "slash", used by some CSS frameworks as grid element width selector.

While this is still opinionated, as most characters will work as escaped character in a Stylesheet file, while there is no need to escape those in a tag attribute.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

pcate’s picture

I tested the patch and it works great!

While this is still opinionated, as most characters will work as escaped character in a Stylesheet file, while there is no need to escape those in a tag attribute.

Opinionated yes, but it is a pragmatic fix by adding support for the 2 most common escaped characters CSS frameworks use.

pcate’s picture

Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, 6: valid-css-identifer-characters-2916377-6.patch, failed testing. View results

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

chris burge’s picture

The issue isn't with the slash and the '@' sign, specifically. It's with escaped characters more generally. There's a sister issue at #3050007: CSS identifiers created via `Html::cleanCssIdentifier` shouldn't strip colons where they're solving for colons (but not for slash or '@' sign).

pcate’s picture

I'm wondering if the best way to solve this issue is to use a configuration flag/setting similar to the $conf['allow_css_double_underscores'] setting in D7 that allowed BEM double underscore CSS selectors.

A similar flag could allow developers to set what escaped characters they want to whitelist with the cleanCssIdentifier function. By default it could be none, which wouldn't cause a BC with older D8 versions.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

jptillman’s picture

Is this being worked on by anyone? Drupal8 is currently not compatible with Tailwindcss classes due to this problem -- specifically because it uses "/" in class identifiers. Therefore, these classes can't be used in Views configs and similar. I'd like to fix this somehow.

I'm very confused about how the submitted patches actually worked, since there's a line above the patch-modded lines which uses the default filter (which specifies a "/" should be changed to "-") to modify the identifier.

Perhaps the testers only cared about "@" et al, and not "/"?

pcate’s picture

@jptillman I don't believe anyone is working on this issue. @Daniel Kulbe patch only solves for the @ symbol. @Chris Burge links to a separate issue dealing with the ":" symbol.

Ideally there would be one issue working to resolve this for all escaped characters, but that would require more coordination and probably input from core maintainers.

There is some question as to whether this would constitute a BC or not. I proposed a configuration flag/setting similar to D7's $conf['allow_css_double_underscores'] method if it was, so the fix could be used until D10 when it could be set as the default.

If you would like to start things by creating a patch that fixes the issue for all escape characters you can assign the issue to yourself while working on it.

chris burge’s picture

I agree we should wrap any of the related issues into a single issue and handle them together.

I hadn't thought about the BC aspect, but that will be the challenging part. Here's the risk: we change how ::cleanCssIdentifier() processes strings and one or more class names cleaned with this method are changed. The result would be CSS regressions on sites.

Here's an alternative to adding a flag: Deprecate ::cleanCssIdentifier() and add a new method. The problem with that approach is that ::cleanCssIdentifier() is used all over core. This solution helps with contrib but not with core.

Adding a flag in config might be the way to go.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

pcate’s picture

I did some research into some of the most popular CSS frameworks to try and get a sense of what escaped special characters are being used in the wild. Below are my findings.

If anyway knows of others please add.

inuitcss
@ /

Tailwind
: .

UIKit
@

U.S. Web Design System (USWDS)
:

heddn’s picture

Status: Needs work » Needs review
StatusFileSize
new1.18 KB
new1.14 KB

Status: Needs review » Needs work

The last submitted patch, 21: 2916377-21.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
StatusFileSize
new463 bytes
new1.37 KB

Status: Needs review » Needs work

The last submitted patch, 23: 2916377-22.patch, failed testing. View results

pcate’s picture

@heddn we'll also need to exclude the period character . for compatibility with TailwindCSS 2.0. https://tailwindcss.com/docs/margin is one example of this.

mglaman’s picture

Assigned: Unassigned » mglaman

Adding U+002E for the period character supports margin in Tailwind.

[^\x{002D}\x{002E}\x{002F}\x{003A}\x{0030}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]

https://www.phpliveregex.com/p/yxz

Working on a patch and test fixes.

mglaman’s picture

Assigned: mglaman » Unassigned
Status: Needs work » Needs review
StatusFileSize
new5.66 KB
new5.43 KB

Here is a patch and I fixed the DisplayBlockTest::testBlockEmptyRendering rendering test. But it seems like supporting : and . in class names will cause backwards compatibility problems in class names.

Status: Needs review » Needs work

The last submitted patch, 27: 2916377-27.patch, failed testing. View results

jptillman’s picture

Any chance someone might roll a patch for D8? It's a problem there, too, and the latest patch won't apply.

suresh prabhu parkala’s picture

Status: Needs work » Needs review
StatusFileSize
new5.64 KB

Re-rolled patch. Please review.

Status: Needs review » Needs work

The last submitted patch, 30: 2916377-30.patch, failed testing. View results

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

heddn’s picture

Version: 9.3.x-dev » 10.0.x-dev
Status: Needs work » Needs review
StatusFileSize
new6.31 KB

Re-roll. Conflicts where in the test class.

pcate’s picture

I'm tagging this issue as "Needs frontend framework manager review" since I think this issue needs guidance on if/how to handle the BC aspect of this change, given its potential scope of impact.

dakwamine’s picture

I cannot use underscore in views CSS classes; it gets replaced by a hyphen.

screenshot views
DOM

It goes through Html::cleanCssIdentifier(...). The underscore is removed by the str_replace which uses the $filter character list (which contains the underscore by default), even before the preg_replace processes the identifier.

  public static function cleanCssIdentifier($identifier, array $filter = [
    ' ' => '-',
    '_' => '-',
    '/' => '-',
    '[' => '-',
    ']' => '',
  ]) {
    // We could also use strtr() here but its much slower than str_replace(). In
    // order to keep '__' to stay '__' we first replace it with a different
    // placeholder after checking that it is not defined as a filter.
    $double_underscore_replacements = 0;
    if (!isset($filter['__'])) {
      $identifier = str_replace('__', '##', $identifier, $double_underscore_replacements);
    }
    $identifier = str_replace(array_keys($filter), array_values($filter), $identifier);
    // Replace temporary placeholder '##' with '__' only if the original
    // $identifier contained '__'.
    if ($double_underscore_replacements > 0) {
      $identifier = str_replace('##', '__', $identifier);
    }

The line with

$identifier = str_replace(array_keys($filter), array_values($filter), $identifier);

converts valid underscore into hyphens.

If I remove the default underscore value in the argument, it works.

  public static function cleanCssIdentifier($identifier, array $filter = [
    ' ' => '-',
//    '_' => '-',
    '/' => '-',
    '[' => '-',
    ']' => '',
  ]) {

I understand this change is prone to create unexpected results, especially when existing theme CSS classes were based on the previous behaviour. Alas, I don't have any idea on how views can customize the filter variable.

dakwamine’s picture

And with the previous test, I have the class in both formats. Not sure why.

DOM 2

dakwamine’s picture

After further investigation, now I know why there are both formats. There is a backwards compatibility functionality in the template_preprocess_views_view which calls the cleanCssIdentifier:

// @todo https://www.drupal.org/project/drupal/issues/2977950 Decide what to
// do with the backwards compatibility layer.

cleanCssIdentifier is called in a callback passed to an array_map in views.theme.inc, which may be reworked for an additional option alongside the css_class one. So this is a views module specific processing which is not really related to this very issue.

Regarding our issue here, I was in a rush, so I decided to patch the cleanCssIdentifier method by doing a simple check on the identifier:

  public static function cleanCssIdentifier($identifier, array $filter = [
    ' ' => '-',
    '_' => '-',
    '/' => '-',
    '[' => '-',
    ']' => '',
  ]) {
    if (strpos($identifier, 'keep_underscores___') === 0) {
      unset($filter['_']);
      $identifier = substr($identifier, strlen('keep_underscores___'));
    }
...

... so that anywhere I want to keep the underscores, I only need to prefix my class with the 'keep_underscores___' string. Not the prettiest way to handle this, but it works and does not impact existing classes.

heddn’s picture

StatusFileSize
new6.84 KB

Re-roll.

Status: Needs review » Needs work

The last submitted patch, 38: 2916377-38.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
StatusFileSize
new1.4 KB

This shouldn't make test failures any worse. The css selectors in the block test were already re-worked and aren't in need of updates any longer.

Status: Needs review » Needs work

The last submitted patch, 40: 2916377-40.patch, failed testing. View results

scotwith1t’s picture

Incredibly important patch for anyone using the USWDS framework and many others. Thanks @heddn and others! Not sure what else is needed to get this to land, other than passing tests, but hope to see this one in a release soon! :)

scotwith1t’s picture

Just an additional note that this has a side effect of changing how clean_class twig filter transforms variables into class names (since it uses cleanCssIdentifier). For example, we had a block which had:

{% set classes = [
  'block',
  'block-' ~ configuration.provider|clean_class,
  'block-' ~ plugin_id|clean_class,
] %}

Previously, the plugin_id had its : stripped out and the css was written and applied to that class. After applying the patch in #40, the class name changed because clean_class no longer stripped that colon out of the plugin_id. Not a huge deal, but something I hadn't anticipated and had to do a (very minor) css update to address. I would assume there could be other unexpected side effects for other places/modules that use the cleanCssIdentifier method of this class.

Version: 10.0.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

nikolabintev’s picture

StatusFileSize
new1.29 KB

I am uploading a patch that can be applied to the latest stable Drupal 10 version: 10.2.4

gareth.poole’s picture

StatusFileSize
new1.3 KB

This patch seems to work with 10.3

liberatr’s picture

The patch in #46 removes the comment that mentions period:
// - the period (U+002E)

It is still present in regex, just not in comment.

sea2709’s picture

IMHO, I agree that we should allow more characters in CSS classes like Tailwind CSS classes with colon, brackets, etc. But I'm concerned when we makes changes for the function cleanCssIdentifier, since this function is used to generate a string/class for use as a CSS identifier.

For example, I place a field block in layout builder, that field block has a class like block-field-block:paragraph:grid:field-column if we allow colons in cleanCssIdentifier, originally, this class is block-field-blockparagraphgridfield-column.

I wonder if generated classes should have colons, and it might impact an existing site which has many elements being styled based on the generated classes. We should think twice before applying changes for cleanCssIdentifier.

chris burge’s picture

@sea2709 - That's exactly the issue. We can't change Html::cleanCssIdentifier without causing CSS bugs.

#13

I'm wondering if the best way to solve this issue is to use a configuration flag/setting similar to the $conf['allow_css_double_underscores'] setting in D7 that allowed BEM double underscore CSS selectors.

I think the behavior should be configurable. Maybe a $setting to define additional characters?

yazzbe’s picture

> I think the behavior should be configurable. Maybe a $setting to define additional characters?

That is a great idea!

Just like in the block_class module where it is also configurable to allow special characters in classes.

liberatr’s picture

StatusFileSize
new1.33 KB

Rerolled the 10.3 patch from #46 to include the missing comment.

ibbwebguy’s picture

I believe the order of unicode characters is wrong and will result in unwanted characters being valid. The correct order should be:

$identifier = preg_replace('/[^\x{002D}\x{002E}\x{002F}\x{0030}-\x{0039}\x{003A}\x{0040}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);

Alternately, the syntax could be shortened to the following, but then the alphanumeric ranges would not be specifically listed.
$identifier = preg_replace('/[^\x{002D}-\x{003A}\x{0040}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);

ibbwebguy’s picture

StatusFileSize
new1.36 KB

Rerolled the 10.3 patch from #51 to include the correct unicode regex.

woldtwerk’s picture

StatusFileSize
new1.33 KB

11.2 patch

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

kopeboy’s picture

Even ":" (colon) is not allowed, but it's used by TailwindCSS. For example I can't enter grid gap-4 md:grid-cols-2 xl:grid-cols-3 in the View CSS class.