Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handling AttributeSelector matcher #207

Open
thescientist13 opened this issue Oct 14, 2022 · 5 comments
Open

handling AttributeSelector matcher #207

thescientist13 opened this issue Oct 14, 2022 · 5 comments

Comments

@thescientist13
Copy link

thescientist13 commented Oct 14, 2022

Hello!

I'm a little stuck on how to handle this case where an AttributeSelector has a matcher property.

Given this rule

pre[class*="language-"] {
  color: #ccc;
  background: none;
}

I would want to produce this output (basic minification).

pre[class*="language-"]{color:#ccc;background:none;}

What I'm having trouble with is figuring how the Identifier of class and / or the String of "language-" would be aware of being in the context of an AttributeSelector so I would be able to accurately add the matcher in between them?

Using ASTExplorer, I would get a tree like this

{
  "type": "AttributeSelector",
  "loc": {
    "source": "<unknown>",
    "start": {
      "offset": 3,
      "line": 1,
      "column": 4
    },
    "end": {
      "offset": 23,
      "line": 1,
      "column": 24
    }
  },
  "name": {
    "type": "Identifier",
    "loc": {
      "source": "<unknown>",
      "start": {
        "offset": 4,
        "line": 1,
        "column": 5
      },
      "end": {
        "offset": 9,
        "line": 1,
        "column": 10
      }
    },
    "name": "class"
  },
  "matcher": "*=",
  "value": {
    "type": "String",
    "loc": {
      "source": "<unknown>",
      "start": {
        "offset": 11,
        "line": 1,
        "column": 12
      },
      "end": {
        "offset": 22,
        "line": 1,
        "column": 23
      }
    },
    "value": "language-"
  },
  "flags": null
}

If I were to log the node types of that CSS

const ast = parse(/* ... */);

walk(ast, {
  enter(node) {
    console.debug(node.type);
  }
})

The output I would get is the following

{ type: 'StyleSheet' }
{ type: 'Rule' }
{ type: 'SelectorList' }
{ type: 'Selector' }
{ type: 'TypeSelector' }
{ type: 'AttributeSelector' }
{ type: 'Identifier' }
{ type: 'String' }
{ type: 'Block' }
{ type: 'Declaration' }
{ type: 'Value' }
{ type: 'Hash' }
{ type: 'Declaration' }
{ type: 'Value' }
{ type: 'Identifier' }

Since AttributeSelector is walked first, and that contains the value of matcher, when the Identifier and String nodes are visited, how would I know when to capture the matcher and put that value in between them to achieve this?

class*="language-"

Thanks!

@thescientist13
Copy link
Author

For now, implemented this as an interim solution

leave: function(node, item) {
  switch (node.type) {
    ...
 
    case 'AttributeSelector':
      if (node.matcher) {
        optimizedCss = optimizedCss.replace(`${node.name.name}'${node.value.value}'`,`${node.name.name}${node.matcher}'${node.value.value}'`);
      }
      optimizedCss += ']';
      break;
  }
}

@lahmatiy
Copy link
Member

Sorry, but I didn't get your question. A given and want selectors are look the same for me. If you need to change a matcher, you need to catch a AttributeSelector node and change matcher with what you need.

@thescientist13
Copy link
Author

thescientist13 commented Dec 15, 2022

Thanks for the reply and apologies, I should have included a better actual vs expected there.

So specifically in this part of the snippet

[class*="language-"]

Even though the Identifier (class) and String ('language-') are not children of AttributeSelector, it looks they get walked anyway and so sequentially the values are coming through in this order

  1. *= (matcher)
  2. class (Identifier)
  3. 'language-' (String)
Walking AttributeSelector with value or name =>  {
  value: { type: 'String', loc: null, value: 'language-' },
  name: { type: 'Identifier', loc: null, name: 'class' }
}

Walking Identifier with value or name =>  { value: undefined, name: 'class' }

Walking String with value or name =>  { value: 'language-', name: undefined }

What is making it a little challenging to me is how to know when I can use that matcher value from AttributeSelector in between its own Identifier and String given the order provided by CSSTree

  • If I do it on enter of AttributeSelector, it would come out as pre[*=class'language-']
  • If I do it on leave of AttributeSelector, it would come out as pre[class'language-'*=]
  • There is no this helper context to know if this Identifier or String have a parent of AttributeSelector (from what I can tell)

I made a demo repo you can check out to see how I am approaching it
https://github.com/thescientist13/csstree-attribute-selector-example

And you can load it directly in your browser in a Stackblitz too.
https://stackblitz.com/github/thescientist13/csstree-attribute-selector-example

Hope that makes sense and let me know if there's any more info I can provide! 🙏


Just to add context, although it looks like what I want is the same as the input, I'm using CSSTree to minify the input, hence the string concat in the demo, so for this example it's to remove whitespace and line breaks from the final output CSS.

So starting input would be this (whitespace and line breaks)

pre[class*="language-"] {
  color: white;
}

With the desired output (no whitespace and line breaks)

pre[class*="language-"]{color: white;}

@lahmatiy
Copy link
Member

I see. The walker's API is not intended for AST serialisation. You need to use generate(ast) method instead (see docs). Currently it provides compact formatting only. Luckily, that's what you need.

There are several ways on how you can alter a serialisation:

  • Override generate method of desired nodes using fork() method:
import { fork } from 'css-tree';

const mySyntax = fork({
    node: {
        AttributeSelector: {
            generate() { ... }
        }
    }
});

See lib/syntax/node folder for examples on implementing generate() method.

  • Using decorator option for generate() method. Unfortunately, it's undocumented well, but you can take a look how it can be used in source map decorator (you need to modify handlers argument to add or override a functionality).

@thescientist13
Copy link
Author

thescientist13 commented Dec 22, 2022

I see. The walker's API is not intended for AST serialisation. You need to use generate(ast) method instead (see docs). Currently it provides compact formatting only. Luckily, that's what you need.

OH, haha. Yes, that's pretty convenient indeed, can't believe I missed that one 😅

There are several ways on how you can alter a serialisation:

This is good to know because I do a little light bundling in my more complete solution, by also inlining relative @import paths.

So for example this

/* theme.css */ 
:root {
  --primary-color: red;
  --secondary-color: blue;
} 
/* main.css */
import ./theme.css

body {
  color: var(--primary-color);
}

Would come out as this, (plus minification)

/* main.min.css */
:root {
  --primary-color: red;
  --secondary-color: blue;
}

body {
  color: var(--primary-color);
}

So seems like I would have a similar hook to do a tranformation like that?

walk(ast, {
  enter: function (node, item) { 
    const { type, name, value } = node;

    if ((type === 'String' || type === 'Url') && this.atrulePrelude && this.atrule.name === 'import') {
      const { value } = node;

      if (value.indexOf('.') === 0) {
        const location = path.resolve(path.dirname(url), value);
        const importContents = fs.readFileSync(location, 'utf-8');

        optimizedCss += bundleCss(importContents, url);  // recursive function to keep walking all nested relative @import
      } else {
        optimizedCss += `@import url('${value}');`;
      }
   } else {
     ...
   }
}

Do you think that would still be feasible with generate?

Thanks so much for your help and for maintaining this project! ✌️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants